并发编程二、线程的安全性和线程通信
一、线程的安全
线程的合理使用能够很好的提高程序的处理性能,第一个是利用多核CPU多线程来实现线程的并行操作,第二个是线程的异步化执行相比于同步执行来说,异步化执行能够很好地优化程序的处理性能提升并发吞吐量。
同时也带来了一些麻烦,以下面简单的例子来说。
1. 一个问题来讨论线程安全性问题
样例:启动1000个线程去执行ThreadCount的add()方法。
import java.util.concurrent.TimeUnit;
public class ThreadCount {
private static int count = 0;
public static void add() {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count ++;
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i ++) {
new Thread(() -> {
ThreadCount.add();
}).start();
}
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
...执行结果
992
...执行结果
1000
...执行结果
985
在以上样例内,理论上1000个线程执行了1000次的add()方法,每次count+1,最终值应该为1000。实际结果却是<=1000。为什么会这样呢,其实我们可以先看下count++
操作对应的底层JVM指令:
在target目录找到对应class文件ThreadCount.class,使用javap
命令对其进行反解析:javap -v ThreadCount.class
,结果如下:
public static void add();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=1, args_size=0
0: getstatic #2 // Field java/util/concurrent/TimeUnit.MILLISECONDS:Ljava/util/concurrent/TimeUnit;
3: lconst_1
4: invokevirtual #3 // Method java/util/concurrent/TimeUnit.sleep:(J)V
7: goto 15
10: astore_0
11: aload_0
12: invokevirtual #5 // Method java/lang/InterruptedException.printStackTrace:()V
15: getstatic #6 // Field count:I
18: iconst_1
19: iadd
20: putstatic #6 // Field count:I
23: return
...
可以看到 count++ 对应的JVM指令为:
15: getstatic #6 // Field count:I
18: iconst_1
19: iadd
20: putstatic #6 // Field count:I
...
结合JVM指令集,可以得出`count ++`一个简单的操作,其实是对应四个步骤的操作,并不是一个原子性的操作:
15: getstatic #6 // Field count:I 从类中获取静态字段
18: iconst_1 将int类型常量1压入栈
19: iadd 执行int类型的加法
20: putstatic #6 // Field count:I 设置类中静态字段的值
此时可以再回到之前的例子,1000个线程来执行count++:
线程A、B、C..并发执行,如果线程A执行到了getstatic
指令或者iadd
指令,从内存中获取到了值或者已经修改了值但并没有执行putstatic
指令将值放入内存中;
此时另外的线程B获取到时间片执行了getstatic
指令获取到了count的值;
之后时间片交由线程A,A执行putstatic
将结果更新入内存中;
然后线程B也执行iadd
操作并将值更新入内存中,此时线程B的putstatic
会将线程A的操作覆盖掉,也就造成了count最终小于1000的结果。由此引发了线程安全性问题。
2. 线程安全性
由上面的例子引出了一个问题:java中如何保证由于线程并行导致的数据访问的安全性问题?
synchronized的基本用法
在数据库中为了防止对同一条数据进行并行的操作,提供了行锁、表锁等;Java也提供了synchronized关键字来实现对数据的加锁。
synchronized有三种方式来枷锁,不同的修饰类型,代表锁的控制粒度:
对象锁 : 修饰实例方法,或者 synchronized(this) 以当前实例对象为锁
类锁 : 修饰静态方法,或者 synchronized(A.class) 以当前类对象为锁
代码块加锁 : 对方法内一部分代码块进行加锁
对象锁
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class ObjectSynchronized {
private static int num = 0;
//对象锁
public synchronized void add() {
System.out.println(Thread.currentThread().getName() + " start sleep-- " + new Date());
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end sleep-- " + new Date());
num ++;
System.out.println(Thread.currentThread().getName() + " end sleep-- " + num + " --- " + new Date());
}
public void inc() {
// 对象锁
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " start sleep-- " + new Date());
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
num ++;
System.out.println(Thread.currentThread().getName() + " end sleep-- " + new Date());
System.out.println(Thread.currentThread().getName() + " end sleep-- " + num + " --- " + new Date());
}
}
public static void main(String[] args) {
ObjectSynchronized sync = new ObjectSynchronized();
Thread thread1 = new Thread(()-> {
sync.add();
}, "t1");
Thread thread2 = new Thread(()-> {
sync.inc();
}, "t2");
thread1.start();
thread2.start();
}
}
...运行结果
t1 start sleep-- Mon May 18 17:24:50 CST 2019
t1 end sleep-- Mon May 18 17:25:00 CST 2019
t1 end sleep-- 1 --- Mon May 18 17:25:00 CST 2019
t2 start sleep-- Mon May 18 17:25:00 CST 2019
t2 end sleep-- Mon May 18 17:25:10 CST 2019
t2 end sleep-- 2 --- Mon May 18 17:25:10 CST 2019
可以看到,两个线程同时启动,只有在其中一个线程执行完毕,释放锁后,另外一个线程才能获取到该锁。
可见,synchronized修饰方法和synchronized(this)本质上是一样的,都是以一个对象为锁。
类锁
三个线程同时启动,分别访问synchronized修饰的静态方法、 synchronized(class)、及synchronized修饰的普通方法
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class ClassSynchronized {
private static int num = 0;
// 类锁
public static synchronized void add() {
System.out.println(Thread.currentThread().getName() + " start sleep-- " + new Date());
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end sleep-- " + new Date());
num ++;
System.out.println(Thread.currentThread().getName() + " end sleep-- " + num + " --- " + new Date());
}
// 类锁
public void inc() {
synchronized (ClassSynchronized.class) {
System.out.println(Thread.currentThread().getName() + " start sleep-- " + new Date());
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
num ++;
System.out.println(Thread.currentThread().getName() + " end sleep-- " + new Date());
System.out.println(Thread.currentThread().getName() + " end sleep-- " + num + " --- " + new Date());
}
}
// 对象锁
public synchronized void add2() {
System.out.println(Thread.currentThread().getName() + " start sleep-- " + new Date());
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " end sleep-- " + new Date());
num ++;
System.out.println(Thread.currentThread().getName() + " end sleep-- " + num + " --- " + new Date());
}
public static void main(String[] args) {
Thread thread1 = new Thread(()-> {
ClassSynchronized.add();
}, "t1");
ClassSynchronized sync = new ClassSynchronized();
Thread thread2 = new Thread(()-> {
sync.inc();
}, "t2");
Thread thread3 = new Thread(()-> {
sync.add2();
}, "t3");
thread1.start();
thread2.start();
thread3.start();
}
}
... 运行结果
t1 start sleep-- Mon May 18 18:39:29 CST 2019
t3 start sleep-- Mon May 18 18:39:29 CST 2019
t3 end sleep-- Mon May 18 18:39:39 CST 2019
t1 end sleep-- Mon May 18 18:39:39 CST 2019
t3 end sleep-- 1 --- Mon May 18 18:39:39 CST 2019
t1 end sleep-- 2 --- Mon May 18 18:39:39 CST 2019
t2 start sleep-- Mon May 18 18:39:39 CST 2019
t2 end sleep-- Mon May 18 18:39:49 CST 2019
t2 end sleep-- 3 --- Mon May 18 18:39:49 CST 2019
可见,线程t1、t2都是请求的类锁,不能同时执行而必须另一个线程释放类锁才会执行;t3线程可以和t1线程并行,而t3使用的是对象锁,所以对象锁和类锁是两种不同的锁。
至此,可以通过加锁来保证最开始的1000个线程的安全性问题了:
类锁和对象锁两种方式均可以实现:
import java.util.concurrent.TimeUnit;
public class ThreadCount {
private static int count = 0;
private static Object lock = new Object();
// 类锁
// public static void add() {
//
// synchronized (ThreadCount.class) {
// try {
// TimeUnit.MILLISECONDS.sleep(1);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// count ++;
// }
//
// }
// 对象锁
public static void add() {
synchronized (lock) {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count ++;
}
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i ++) {
new Thread(() -> {
ThreadCount.add();
}).start();
}
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
...运行结果
1000
至此,我们可以得出一些结论:多个线程访问同一个共享资源时,可以通过加锁来保证线程安全,而且多个线程所使用的锁必须是同一个。
那显然每个线程在访问加锁资源时,会尝试先去拿到锁,那这个锁是放在什么地方的呢,为什么随便一个Object对象都可以是一把锁?这个锁里面由哪些内容组成,是不是会记录当前持有锁的线程?这些问题都要在锁的存储里找到答案。
锁的存储
在使用synchronized(lock)
时候,随便一个Object对象都可以成为锁,那为什么随便一个Object对象都可以成为锁呢?
或许我们可以以这个lock对象为出发点,分析该对象在jvm内存中是如何存储的。
在Hotspot虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对其填充(Padding)
对象头又由对象标记(mark word)和类元信息(klass pointer)两部分组成。在mark word中存储了hashcode、分代年龄、锁的标识等。
从下图hotspot的源码当中的注释可以看到,对象头32位和64位存储方式也略有不同。
这样看32位的会更直观一点。
其中无锁和偏向锁的锁标志位都为01,只是前面的1bit区分了具体是无锁还是偏向锁。JDK1.6以后的版本在处理同步锁时存在锁升级的概念,JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。
JVM一般是这样使用锁和Mark Word的:
1,当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
2,当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
3,当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
4,当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般不会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
5,偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
6,轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
7,自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。
有了以上的理论基础之后,我们可以创建一些有锁的、无锁的对象,并使用一些工具包将这些对象的对象头打印出来,进行核实验证。
首先,需要jol
工具包,之后就可以打印对象的对象头信息了。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
无锁对象
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
...运行结果
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
...结果分析 (通过最后三位来看锁的状态和标记)
整个对象一共16字节,其中对象头(object header)12Byte,还有4字节是对其的字节(因为在64位虚拟机上对象的大小必 必须是8的倍数)。
第一行内容`00000001`与锁状态的匹配:
unused:1byte | 分代年龄:4bit | 是否偏向锁:1bit | 锁标志位:2bit |
0 000 0 01
结合上面32位的对象头存储图,可以看到:锁标志位为`01`,是否偏向锁为`0`,为无锁状态,和Java代码逻辑一致。
偏向锁
偏向锁的设计初衷是针对只有一个线程频繁访问的资源。在这种情况下,锁不仅仅不存在竞争,而且都是由同一个线程多次获得。当线程第一次访问锁对象时,会在对象头内存储当前线程的ID,当这个线程重复访问时,会从锁的对象头内取出当前占用的线程ID和其比较,如果一致则为同一个线程,偏向锁偏向的就是当前线程,后续该线程进入和退出这个加锁区域时不会再次加锁和释放锁,减少了开销。
public static void main(String[] args) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object lock = new Object();
synchronized (lock) {
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
...运行结果
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 38 8a 02 (00000101 00111000 10001010 00000010) (42612741)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
...结果分析
第一行内容`00000101`与锁状态的匹配:
unused:1byte | 分代年龄:4bit | 是否偏向锁:1bit | 锁标志位:2bit |
0 000 1 01
是否偏向锁为1,锁标志位为01,为偏向锁。
在JDK1.6以后默认开启了偏向锁这个优化。在实际应用开发中,如果绝大多数情况是2或者多个线程来竞争,如果开启偏向锁,反而会提升获取锁的资源消耗。我们可以通过在启动JVM的时候加上-XX:-UseBiasedLocking
参数来禁用偏向锁。
轻量级锁
如果偏向锁被关闭或者当前偏向锁已经被其它线程所占用,那么其它线程在抢占锁时,锁会升级到轻量级锁。
在升级到轻量级锁的过程中,使用到了自旋锁,所谓自旋,即当线程去访问已经加锁的资源时,会在原地循环等待,相当于执行一个什么也不做的for循环,直到这个线程获取到锁。这样的操作无疑相当消耗CPU,所以轻量级锁在使用自旋时默认会有次数限制,自旋次数默认10次,可以通过preBlockSpin
参数调整,并且轻量级锁默认是认为线程原地等待就能很快获得锁,适用于同步代码块执行很快的场景。
JDK1.6后也对自旋锁进行了升级,引入了自适应自旋锁,即可以自适应,不会像以前一样尝试固定次数的自旋,而是根据前一次在同一个锁上的自旋时间和锁的拥有着的状态来决定的。比如对于某个锁,以前自旋几次很容易就能获得锁,那么这次也可以多自旋几次;如果以前自旋很少能获得锁,那么就认为自旋用处不大,执行很少的次数或者不执行自旋,直接阻塞线程,避免了处理器资源的浪费。
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
}
...运行结果
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 98 f7 76 02 (10011000 11110111 01110110 00000010) (41351064)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
... 结果分析
锁标志位为00,对应轻量级锁。
重量级锁
适用于多个线程竞争同一把锁的情况,重量级锁会阻塞和唤醒加锁的线程。
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);
Object lock = new Object();
Thread thread1 = new Thread(){
@Override
public void run() {
synchronized (lock){
System.out.println("t1 抢占到锁");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
try {
//让线程晚点儿死亡,造成锁的竞争
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread thread2 = new Thread(){
@Override
public void run() {
synchronized (lock){
System.out.println("t2 抢占到锁");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread1.start();
thread2.start();
}
... 运行结果
t1 抢占到锁
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0a f5 00 1d (00001010 11110101 00000000 00011101) (486601994)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
t2 抢占到锁
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0a f5 00 1d (00001010 11110101 00000000 00011101) (486601994)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
... 结果分析
两个线程对应的锁标志位都为`10`,是重量级锁。
总结
偏向锁:内部会存储当前获得锁的线程,当同一线程多次访问,不需要重新获得和释放锁资源。针对一个线程频繁使用锁对象的场景性能提升较大。比较理想化
轻量级锁:通过一定次数的自旋多次CAS来获得锁,相当于不停for循环耗费cpu。使用场景:每个线程可以很快使用完锁并释放
重量级锁:通过将不能获取到锁的线程阻塞、放入等待队列,在使用完锁的线程释放锁后,从队列中取出线程唤醒来处理
二、线程的通信
之前的例子中使用synchronized时发现,多线程访问同一把锁时,其中一个线程如果获取到锁,那么其余线程其实是被阻塞BLOCKED
的,而这些线程具体什么时候被唤醒,取决于获取锁的线程什么时候执行完同步代码块并释放锁。那能不能做到显示的控制呢?
这里我们就可以借助java Object对象的wait、notify、notifyAll来控制线程状态。
wait、notify、notifyAll基本概念
wait: 表示持有对象锁的线程A准备释放对象锁的权限,释放cpu资源并进入等待状态。
notify: 表示持有对象锁的线程A准备释放对象锁的权限,并通知JVM唤醒某一个竞争该对象锁的线程X。
notifyAll: notifyAll和notify的区别在于,notifyAll会唤醒所有竞争这一个对象锁的所有线程,当其中之一X线程竞争到锁并执行完毕释放锁后,会通知余下线程竞争该对象锁。
注意:
notify只会唤醒等待队列中的其中一个线程X,当这个线程X被唤醒并执行完毕,不会自动唤醒队列中的余下线程。其余线程继续等待直至下次notify或者notifyAll。
三个方法都必须在synchronized同步关键字所修饰的作用域中调用,否则会报 java.lang.IllegalMonitorStateException 异常。
打开IllegalMonitorStateException类源码,可以看到如下注释:
/**
* Thrown to indicate that a thread has attempted to wait on an
* object's monitor or to notify other threads waiting on an object's
* monitor without owning the specified monitor.
*
* @author unascribed
* @see java.lang.Object#notify()
* @see java.lang.Object#notifyAll()
* @see java.lang.Object#wait()
* @see java.lang.Object#wait(long)
* @see java.lang.Object#wait(long, int)
* @since JDK1.0
*/
意思就是说:抛出该异常是为了表明当前线程尝试等待对象锁或者通知其它线程等待对象锁,但本身并不拥有该对象锁。这也说明了这几个方法必须在拥有对象锁的情况下调用,即在synchronized代码块内调用。
样例:创建一个Object对象作为对象锁,3个线程分别使用该锁,并在线程内部wait来等待唤醒,在主线程内获取该锁notify/notifyAll来唤醒等待线程。
public static void main(String[] args) throws InterruptedException {
Object lock = new Object();
Thread t1 = new Thread(()-> {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 获得锁-- " + new Date());
try {
System.out.println(Thread.currentThread().getName() + " 准备wait,释放锁-- " + new Date());
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 获得锁-- " + new Date());
System.out.println(Thread.currentThread().getName() + " 运行完毕,释放锁 " + new Date());
}
}, "t1");
Thread t2 = new Thread(()-> {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 获得锁-- " + new Date());
try {
System.out.println(Thread.currentThread().getName() + " 准备wait,释放锁-- " + new Date());
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 获得锁-- " + new Date());
System.out.println(Thread.currentThread().getName() + " 运行完毕,释放锁 " + new Date());
}
}, "t2");
Thread t3 = new Thread(()-> {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 获得锁-- " + new Date());
try {
System.out.println(Thread.currentThread().getName() + " 准备wait,释放锁-- " + new Date());
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 获得锁-- " + new Date());
System.out.println(Thread.currentThread().getName() + " 运行完毕,释放锁 " + new Date());
}
}, "t3");
t1.start();
t2.start();
t3.start();
TimeUnit.SECONDS.sleep(3);
synchronized (lock) {
// System.out.println(Thread.currentThread().getName() + " 获得锁, notify 随机唤醒一个线程-- " + new Date());
// lock.notify();
System.out.println(Thread.currentThread().getName() + " 获得锁, notifyAll 唤醒所有线程-- " + new Date());
lock.notifyAll();
}
}
...notify运行结果: (随机1个线程被唤醒)
t2 获得锁-- Tue May 19 15:40:51 CST 2019
t2 准备wait,释放锁-- Tue May 19 15:40:51 CST 2019
t3 获得锁-- Tue May 19 15:40:51 CST 2019
t3 准备wait,释放锁-- Tue May 19 15:40:51 CST 2019
t1 获得锁-- Tue May 19 15:40:51 CST 2019
t1 准备wait,释放锁-- Tue May 19 15:40:51 CST 2019
main 获得锁, notify 随机唤醒一个线程-- Tue May 19 15:40:54 CST 2019
t2 获得锁-- Tue May 19 15:40:54 CST 2019
t2 运行完毕,释放锁 Tue May 19 15:40:54 CST 2019
...notifyAll运行结果:
t1 获得锁-- Tue May 19 15:39:23 CST 2019
t1 准备wait,释放锁-- Tue May 19 15:39:23 CST 2019
t2 获得锁-- Tue May 19 15:39:23 CST 2019
t2 准备wait,释放锁-- Tue May 19 15:39:23 CST 2019
t3 获得锁-- Tue May 19 15:39:23 CST 2019
t3 准备wait,释放锁-- Tue May 19 15:39:23 CST 2019
main 获得锁, notifyAll 唤醒所有线程-- Tue May 19 15:39:26 CST 2019
t3 获得锁-- Tue May 19 15:39:26 CST 2019
t3 运行完毕,释放锁 Tue May 19 15:39:26 CST 2019
t2 获得锁-- Tue May 19 15:39:26 CST 2019
t2 运行完毕,释放锁 Tue May 19 15:39:26 CST 2019
t1 获得锁-- Tue May 19 15:39:26 CST 2019
t1 运行完毕,释放锁 Tue May 19 15:39:26 CST 2019
生产者、消费者
借助线程的通信来实现生产者、消费者MQ通信
创建一个公共队列,生产者线程往里面添加数据,当队列达到最大值,通知消费者线程消费数据;消费者线程消费完数据,通知生产者线程生产数据...
测试类,创建公共队列、生产者及消费者线程,生产者和消费者线程以公共队列为锁,这样的话两个线程使用的是同一把锁,可以相互唤醒。
import java.util.LinkedList;
import java.util.Queue;
public class MainTest {
public static void main(String[] args) {
Queue<String> queue = new LinkedList<>();
Producer producer = new Producer(queue, 5);
Consumer consumer = new Consumer(queue);
Thread t1 = new Thread(producer, "Producer");
Thread t2 = new Thread(consumer, "Consumer");
t1.start();
t2.start();
}
}
生产者
import java.util.Queue;
public class Producer implements Runnable {
private Integer maxSize;
private Queue<String> queue;
public Producer(Queue<String> queue,Integer maxSize) {
this.queue = queue;
this.maxSize = maxSize;
}
@Override
public void run() {
Integer i = 0;
synchronized (queue) {
while (true) {
i++;
while (queue.size() == maxSize) {
try {
System.out.println("队列已满,生产者等待,消费者开始消费!");
queue.notify(); // 唤醒处于等待的队列
queue.notifyAll();
queue.wait(); //释放锁,当前线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
queue.add(i.toString());
System.out.println(Thread.currentThread().getName() + " 生产了 " + i);
}
}
}
}
消费者
import java.util.Queue;
public class Consumer implements Runnable {
private Queue<String> queue;
public Consumer(Queue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
synchronized (queue) {
while (true) {
while (queue.isEmpty()) {
try {
System.out.println("队列为空,消费者等待,生产者开始生产!");
queue.notify(); //唤醒等待队列
queue.notifyAll();
queue.wait(); //释放锁,当前线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String date = queue.remove();
System.out.println("消费者消费: " + date);
}
}
}
}
执行结果:
队列为空,消费者等待,生产者开始生产!
Producer 生产了 1
Producer 生产了 2
Producer 生产了 3
Producer 生产了 4
Producer 生产了 5
队列已满,生产者等待,消费者开始消费!
消费者消费: 1
消费者消费: 2
消费者消费: 3
消费者消费: 4
消费者消费: 5
队列为空,消费者等待,生产者开始生产!
Producer 生产了 6
Producer 生产了 7
Producer 生产了 8
Producer 生产了 9
Producer 生产了 10
队列已满,生产者等待,消费者开始消费!
消费者消费: 6
消费者消费: 7
...
至此,本篇分析了如何保证线程之间的安全性、几种锁的范围、锁的种类、锁的存储、以及线程之间的通信功能。