线程同步和线程通信
一.线程同步
线程同步的产生:多个线程操纵同一个对象,由于对象的实例变量放在运行时数据区的堆区中,所以这些线程可以同时操作该对象的实例变量,导致一个线程修改另外一个线程的临时操作数,产生结果不正确的现象。
(1)两个线程操作同一个对象,对象的实例变量会受到两个线程的同时修改,因为实例变量在运行时数据区的堆区中,两个线程共享对象的实例变量,而方法的局部变量不存在这样的问题,因为每一个线程都有自己的方法调用栈,用于保存局部变量,局部变量不共享。
(2)在两个线程并发执行的过程中,由于共享实例变量,一个线程可能会修改另外一个线程的临时操作数(也就是这个共享变量),这样会导致共享资源的访问冲突,从而导致结果不正确。
(3)因此,要操纵共享变量,需要使用原子操作,原子操作是一组操作的集合,这组操作的特点是,(3.1)操纵与其他线程的共享资源(为实现这一点,原子操作必须在共享对象的类中定义)(在共享对象类中定义原子操作,这样每个使用共享对象的线程都能够执行原子操作),(3.2)并且一个线程在执行原子操作期间,其他线程不能够操纵共享资源(为实现这一点,必须用同步代码块包装原子操作)(原子操作在共享资源的类中定义);
(4)为了保证每一个线程都能够正常执行原子操作(怎么将一组对共享对象的操纵作为一组原子服务执行)(既操作共享资源,又保证在一个线程操纵共享资源时,没有其他线程操纵共享资源),Java中引入同步机制,在代表原子操作的代码前加上synchronized标记(synchronized从第一条操纵公共资源的语句开始,一直到最后一条操纵公共资源的语句结束,不要多加,也不要少加);
(5)每一个对象都有一个同步锁,在任何时刻,最多只允许一个线程拥有这把锁,只有获得对象的锁,才能够执行对象的synchronized方法,无论是相同的同步代码块,还是不同的同步代码块,只要线程没有对象的锁,那么任意定义在对象类中的synchronized块,线程都没有办法操纵(也就是说,对象唯一的锁+synchronized块共同实现了原子操作,保证一个线程在执行synchronized块<原子操作->操纵共享资源>时,没有其他线程操纵共享资源);
(6)线程同步的特征:
a.只要是对共享资源的操作,都应该设为原子操作,使用synchronized修饰符进行修饰,因为所有的synchronized修饰符都修饰原子操作,因此一个线程的synchronized代码没有执行结束前,其他任何线程都不能够执行synchronized块,因为这样会修改临时数据,导致结果不正确。
b.每一个对象都有且仅有一个同步锁,如果任何线程想要执行对象的synchronized块,必须获得对象的同步锁,如果有一个线程在执行synchronized块,说明它已经得到了对象的同步锁,其他对象要想执行原子操作,必须等待正在执行原子操作的线程释放对象锁(synchronized方法执行结束或者调用对象的wait()方法)
c.静态方法也可以用synchronized修饰,不仅是对象,每一个加载到运行时数据区方法区的类也有唯一的同步锁;
d.执行原子操作的线程可以中断,但是不会释放对象的锁,进入同步代码块的线程可以执行sleep()或者yield()方法,这样会将CPU的使用机会让给其他线程(从可运行池中选一个线程出来,sleep任选一个线程,yield选一个相同或者更高优先级的线程),但是等待锁的线程仍旧处于对象的锁池中,处于阻塞状态。
e.synchronzied特性不会被继承,必须显式指明。
(7)同步代码块的位置
共享对象的类中的第一句对实例变量操纵的语句开始,到最后一句对实例变量操纵的语句结束,还有线程中对共享对象的实例变量的操纵语句。
(8)线程安全
8.1线程安全的定义(一个线程安全的类)
a.这个类的对象可以同时被多个线程安全访问
b.每个线程都能够正确地执行原子操作(为保证这一点,原子操作尽量在共享对象的类中定义),得到正确的结果;
c.线程的原子操作结束后,对象的逻辑正确。
8.2 不可变类永远是线程安全的,因为不可变类的属性不能够被修改,只能够被访问。
(9)释放对象的锁
线程在以下情况下会释放对象的锁
a.同步代码块执行结束
b.线程在执行过程中遇到异常,线程终止,这样既释放锁,又让出CPU
c.线程执行对象的wait()方法,线程会释放对象的锁,进入对象的等待池
总结:处于阻塞状态的线程
位于对象的等待池
位于对象的锁池
调用sleep(),或者执行IO操作
前两中阻塞状态下的线程均没有对象的锁,最后一种情况下线程有对象的锁,但是没有CPU ,注意运行中的线程如果调用线程的yield()方法,不会释放锁,直接进入runnable状态
(10)死锁发生的条件,
至少有两个类的实例,而且这些类的实例被多个线程共享,如果线程A执行对象a的synchronized块,需要操纵对象b的synchronized块,而这时线程B正在执行对象b的synchronized块,拥有对象b的对象锁,线程A进入对象b的锁池,等待线程B释放对象b的对象锁,而线程B在执行synchronized块的过程中需要执行线程A的synchronized块,可是这时对象a的锁被线程A占用,故线程B进入对象a的锁池中,这样,线程A和线程B互相等待对方释放对象锁,进入死锁状态。
要避免死锁发生, 需要保证所有的线程按照相同的顺序访问共享对象
二.线程通信
(1)不同的线程执行不同的操作(既然是不同的操作,必然是不同的线程执行不同的synchronized块),如果这些操作之间有联系,既某个操作必须等另外一个操作执行结束后执行,则执行这些操作的线程之间需要能够进行通信,才能协调完成工作
(2)java.lang.Object类中提供两个用于线程通信的方法
2.1 wait()
a.执行该方法的线程释放对象的锁;
b.JVM将该执行该方法的线程放到对象的等待池中;
c.该线程等待其他线程将它唤醒。
2.2 notify()
a.该方法使JVM从对象的等待池中选出一个线程,放入对象的锁池中 ,放入锁池中后结果还是阻塞状态
Java中,如果两个线程t1和t2共同操作同一个对象,则这两个线程通过wait()和notify()方法进行线程之间的通信
(3)线程通信的过程大概如下:
3.1 线程t1执行对象的synchronized块,持有对象的锁,线程t2在对象的锁池中等待;
3.2 线程t1执行对象的wait()方法,释放对象的锁,被JVM放入对象的等待池中;
3.3 线程t2得到对象的锁,开始执行对象的另外一个synchronized代码块,在执行过程中执行对象的notify()方法,JVM将t1从对象的等待池中取出,放到对象的锁池中,t1等待获得对象的锁
3.4 线程t2通过执行对象的wait()方法或者执行synchronized块结束,释放对象的锁,t1冲洗获得对象的锁后,进入runnable状态,等待CPU的调度。
线程通信的实例
4.wait()和notify()方法必须放在类的synchronized块中;
5.wait()方法必须在一个while循环中进行调用,因为共享资源的实例变量的状态不定,即使线程重新获得了锁,也要判断当前是否能够继续往下执行,还是需要继续执行对象的wait()方法
6.一般在synchronized块开始时就执行对象的notifyAll()语句,因为这时应该执行的线程,已经获得了对象的锁,开始执行,没有必要让对象等待池中的线程一直等待,应该放入锁池中,等待对象的锁。