天天看点

JAVA多线程——实现同步

转载(https://www.cnblogs.com/soundcode/p/6295910.html)加上了自己的补充和理解

为何要使用同步?

java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查), 将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用, 从而保证了该变量的唯一性和准确性。

线程同步的方法

  1. 同步方法:把synchronized当作函数修饰符时,示例代码如下:
public synchronized void aMethod() { 
    // do something 
}
           

这也就是同步方法,那这时synchronized锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法。

上边的示例代码等同于如下代码:

public void methodAAA()

{

synchronized (this)      //  (1)

{

       //…..

}

}
           

(1)处的this指的是什么呢?它指的就是调用这个方法的对象,如P1。可见同步方法实质是将synchronized作用于object reference。――那个拿到了P1对象锁的线程,才可以调用P1的同步方法,而对P2而言,P1这个锁与它毫不相干,程序也可能在这种情形下摆脱同步机制的控制,造成数据混乱:(

问题:这里提到的都是对象锁,那么我调用这个对象的非synchronized方法的时候,是否是同步的呢?

解释:不是同步的,此时别的对象依旧可以访问这个对象的非synchronized方法,例子见对象锁的理解中的脏读部分,thread睡眠的时候,并未释放锁,然而main线程依旧可以访问getvalue

继续问题:我认为同是直接对方法加synchronized,所以他们的监听器应该是同一个对象,所以说那时候的getValue不可访问,相当于就是方法里的代码块synchronized(object),为同一个Object,也就是说能否同步,是要看获得的锁是否是同一个锁

简单说就是:如果两个线程使用了同一个“对象监视器”,运行结果同步,否则不同步.

静态同步synchronized方法与synchronized(class)代码块:

静态同步synchronized方法与synchronized(class)代码块持有的锁一样,都是Class锁,Class锁对对象的所有实例起作用。synchronized关键字加到非static静态方法上持有的是对象锁。

JAVA多线程——实现同步

线程A,B和线程C持有的锁不一样,所以A和B运行同步,但是和C运行不同步。

2 同步代码块

public void method3(SomeObject so)
{
    synchronized(so)
{

       //…..
}
}
           

这时,锁就是so这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的instance变量(它得是一个对象)来充当锁。

如果一个对象既有同步方法,又有同步块,那么当其中任意一个同步方法或者同步块被某个线程执行时,这个对象就被锁定了,其他线程无法在此时访问这个对象的同步方法,也不能执行同步块。

synchronized 关键字用于保护共享数据

例子:

public class ThreadTest implements Runnable{

public synchronized void run(){
  for(int i=;i<;i++) {
    System.out.print(" " + i);
  }
}

public static void main(String[] args) {
  Runnable r1 = new ThreadTest(); //也可写成ThreadTest r1 = new ThreadTest();
  Runnable r2 = new ThreadTest();
  Thread t1 = new Thread(r1);
  Thread t2 = new Thread(r2);
  t1.start();
  t2.start();
}}
           

在这个程序中,run()虽然被加上了synchronized 关键字,但保护的不是共享数据。因为这个程序中的t1,t2 是两个对象(r1,r2)的线程。而不同的对象的数据是不同的,r1,r2 有各自的run()方法,所以输出结果无法预知。

synchronized的目的是使同一个对象的多个线程,在某个时刻只有其中的一个线程可以访问这个对象的synchronized 数据。每个对象都有一个“锁标志”,当这个对象的一个线程访问这个对象的某个synchronized 数据时,这个对象的所有被synchronized 修饰的数据将被上锁(因为“锁标志”被当前线程拿走了),只有当前线程访问完它要访问的synchronized 数据时,当前线程才会释放“锁标志”,这样同一个对象的其它线程才有机会访问synchronized 数据。

此处想看锁标志的源码:简单的看,方法里有一个标志位,来表示对象锁是否被线程访问?

源码剖析synchronized

再看例子:

public class ThreadTest implements Runnable{

public void run(){

    synchronized(this){
    for(int i=;i<;i++){
        System.out.print(" " + i);
    }
} 
}

public static void main(String[] args){
    Runnable r = new ThreadTest();
    Thread t1 = new Thread(r);
    Thread t2 = new Thread(r);
    t1.start();
    t2.start();
}
}  
           

结果应该是:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

而代码:

public class ThreadTest implements Runnable{

public void run(){
  for(int k=;k<;k++){
    System.out.println(Thread.currentThread().getName()+ " : for loop : " + k);
  }

synchronized(this){
  for(int k=;k<;k++) {
    System.out.println(Thread.currentThread().getName()+ " : synchronized for loop : " + k);
  }} }

public static void main(String[] args){
  Runnable r = new ThreadTest();
  Thread t1 = new Thread(r,"t1_name");
  Thread t2 = new Thread(r,"t2_name");
  t1.start();
  t2.start();
} }
           

运行结果:

t1_name : for loop : 

t1_name : for loop : 

t1_name : for loop : 

t2_name : for loop : 

t1_name : for loop : 

t2_name : for loop : 

t1_name : for loop : 

t2_name : for loop : 

t1_name : synchronized for loop : 

t2_name : for loop : 

t1_name : synchronized for loop : 

t2_name : for loop : 

t1_name : synchronized for loop : 

t1_name : synchronized for loop : 

t1_name : synchronized for loop : 

t2_name : synchronized for loop : 

t2_name : synchronized for loop : 

t2_name : synchronized for loop : 

t2_name : synchronized for loop : 

t2_name : synchronized for loop : 
           

第一个for 循环没有受synchronized 保护。

问题出现:在同步代码块的循环体重,输出的时候是调用静态方法currentthread,会对结果产生影响,不调用的时候,结果是两个线程完成一遍循环,而调用的话,则是分别前后完成一遍循环,有点想不清楚了???

3 wait与notify

配合此图学习,wait后进入等待队列,notify唤醒后进入了锁池,此时谁获得cpu会根据线程优先级(相同则随机决定)进入running状态

JAVA多线程——实现同步

a. wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。

b. wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。

c. 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方法(notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放),调用wait方法的一个或多个线程就会解除wait状态,重新参与竞争对象锁,程序如果可以再次得到锁,就可以继续向下运行。。也就是在你notifyall()了之后,之前在wait()的线程都被唤醒了,但是锁有可能没被释放(一般就是当前线程拥有锁,因为notify一般在synchronized代码块中),锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程

看例子:

public class CyclicBarrierTest {  

    public static void main(String[] args) throws Exception {  
        final Sum sum=new Sum();  

        new Thread(new Runnable() {  
            @Override  
            public void  run() {  
                try {  
                    synchronized (sum) {  
                        System.out.println("thread3 get lock");  
                        sum.sum();  
                        sum.notifyAll(); //此时唤醒没有作用,没有线程等待  
                        Thread.sleep();  
                        System.out.println("thread3 really release lock");  
                    }  

                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
            }  
        }).start();  

        new Thread(new Runnable() {  
            @Override  
            public void  run() {  
                try {  
                    synchronized (sum) {  
                        System.out.println("thread1 get lock");  
                        sum.wait();//主动释放掉sum对象锁  
                        System.out.println(sum.total);  
                        System.out.println("thread1 release lock");  
                    }  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
            }  
        }).start();  

        new Thread(new Runnable() {  
            @Override  
            public void  run() {  
                try {  
                    synchronized (sum) {  
                        System.out.println("thread2 get lock");  
                        sum.wait();  //释放sum的对象锁,等待其他对象唤醒(其他对象释放sum锁)  
                        System.out.println(sum.total);  
                        System.out.println("thread2 release lock");  
                    }  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
            }  
        }).start();  
    }  

}  

class Sum{  
    public Integer total=;  

    public void  sum() throws Exception{  
        total=;  
        Thread.sleep();  
    }  

}  
           

输出结果:

thread3 get lock  
thread3 really release lock  
thread2 get lock  
thread1 get lock  
//程序后面一直阻塞  
           

代码解释:线程3得到锁sum,此时唤醒没有用处,因为没有在wait的线程,然后线程3释放锁sum。

线程1得到锁sum,sum.wait(),线程阻塞,锁被释放。

线程2得到锁sum,sum.wait(),线程阻塞,锁被释放。

无人唤醒线程

更改顺序,见代码:

public class CyclicBarrierTest {  

    public static void main(String[] args) throws Exception {  
        final Sum sum=new Sum();  



        new Thread(new Runnable() {  
            @Override  
            public void  run() {  
                try {  
                    synchronized (sum) {  
                        System.out.println("thread1 get lock");  
                        sum.wait();//主动释放sum对象锁,等待唤醒  
                        System.out.println(sum.total);  
                        System.out.println("thread1 release lock");  
                    }  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
            }  
        }).start();  

        new Thread(new Runnable() {  
            @Override  
            public void  run() {  
                try {  
                    synchronized (sum) {  
                        System.out.println("thread2 get lock");  
                        sum.wait();  //主动释放sum对象锁,等待唤醒  
                        System.out.println(sum.total);  
                        System.out.println("thread2 release lock");  
                    }  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
            }  
        }).start();  

        new Thread(new Runnable() {  
            @Override  
            public void  run() {  
                try {  
                    synchronized (sum) {  
                        System.out.println("thread3 get lock");  
                        sum.sum();  
                        sum.notifyAll();//唤醒其他等待线程(线程1,2)  
                        Thread.sleep();  
                        System.out.println("thread3 really release lock");  
                    }  

                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
            }  
        }).start();  


    }  

}  

class Sum{  
    public Integer total=;  

    public void  sum() throws Exception{  
        total=;  
        Thread.sleep();  
    }  

}  
           

输出:

thread1 get lock  
thread2 get lock  
thread3 get lock  
thread3 really release lock  
  
thread2 release lock  
  
thread1 release lock  
           

代码解释:

线程1得到锁sum,释放锁,线程1进入阻塞,等待唤醒。

线程2得到锁sum,释放锁,线程2进入阻塞,等待唤醒。

线程3得到锁sum,锁sum唤醒线程1,2(此时还未释放),线程3释放锁,线程1,2竞争锁(由CPU分配)

线程1先被唤醒执行sum.total,然后释放锁,然后线程2被唤醒,获得锁…….

例子来源:(https://blog.csdn.net/azhegps/article/details/63031562)

d. wait() 需要被try catch包围,中断也可以使wait等待的线程唤醒。

e. notify 和wait 的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。

f. notify 和 notifyAll的区别:notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。

g. 在多线程中要测试某个条件的变化,使用if 还是while?

要注意,notify唤醒沉睡的线程后,线程会接着上次的执行继续往下执行。所以在进行条件判断时候,可以先把 wait 语句忽略不计来进行考虑,显然,要确保程序一定要执行,并且要保证程序直到满足一定的条件再执行,要使用while来执行,以确保条件满足和一定执行。如下代码:

public class K {
      //状态锁
      private Object lock;
      //条件变量
      private int now,need;
      public void produce(int num){
          //同步
          synchronized (lock){
             //当前有的不满足需要,进行等待
             while(now < need){
                 try {
                     //等待阻塞
                     wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 System.out.println("我被唤醒了!");
             }
            // 做其他的事情
         }
     }
 }
             
           

此代码为一模板,具体实现看下面的例子

生产者消费者的问题,来源于(https://www.cnblogs.com/moongeek/p/7631447.html)

基本思想:假设有一个公共的容量有限的池子,有两种人,一种是生产者,另一种是消费者。需要满足如下条件:

    1、生产者产生资源往池子里添加,前提是池子没有满,如果池子满了,则生产者暂停生产,直到自己的生成能放下池子。

    2、消费者消耗池子里的资源,前提是池子的资源不为空,否则消费者暂停消耗,进入等待直到池子里有资源数满足自己的需求。

仓库类:

import java.util.LinkedList;
  
  /**
 4  *  生产者和消费者的问题
 5  *  wait、notify/notifyAll() 实现
 6  */
  public class Storage1 implements AbstractStorage {
      //仓库最大容量
      private final int MAX_SIZE = ;
     //仓库存储的载体
     private LinkedList list = new LinkedList();
 
     //生产产品
     public void produce(int num){
         //同步
         synchronized (list){
             //仓库剩余的容量不足以存放即将要生产的数量,暂停生产
             while(list.size()+num > MAX_SIZE){
                 System.out.println("【要生产的产品数量】:" + num + "\t【库存量】:"
                         + list.size() + "\t暂时不能执行生产任务!");
 
                 try {
                     //条件不满足,生产阻塞
                     list.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
 
             for(int i=;i<num;i++){
                 list.add(new Object());
             }
 
             System.out.println("【已经生产产品数】:" + num + "\t【现仓储量为】:" + list.size());
 
             list.notifyAll();
         }
     }
 
     //消费产品
     public void consume(int num){
         synchronized (list){
 
             //不满足消费条件
             while(num > list.size()){
                 System.out.println("【要消费的产品数量】:" + num + "\t【库存量】:"
                         + list.size() + "\t暂时不能执行生产任务!");
 
                 try {
                     list.wait();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
 
             //消费条件满足,开始消费
             for(int i=;i<num;i++){
                 list.remove();
             }
 
             System.out.println("【已经消费产品数】:" + num + "\t【现仓储量为】:" + list.size());
 
             list.notifyAll();
         }
     }
 }
           

代码解释:

生产的时候,正常生产(容量足够),不进入while循环,只是一个List.add()的调用
,当容量不够的时候,进入while循环,线程阻塞,释放锁list,线程等待被唤醒(其实
就是等待消费者来消费直到仓库容量足够),此时消费者进来(获得锁list),正常消费
的情况下,即调用list.remove,完成后,唤醒锁,且synchronized代码块执行完毕,
释放锁List,此时继续从生产线程wait()后开始,继续判断是否足够生产,如果足够,
进行正常生产,然后唤醒消费的线程(如果此时消费线程处于等待,即不够消费的情况),
释放锁。
           

4 使用特殊域变量(volatile)实现线程同步

a.volatile关键字为域变量的访问提供了一种免锁机制

b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新

c.因此每次使用该域就要重新计算,而不是使用寄存器中的值

d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

原子操作?

class Bank {
            //需要同步的变量加上volatile
            private volatile int account = ;

            public int getAccount() {
                return account;
            }
            //这里不再需要synchronized 
            public void save(int money) {
                account += money;
            }
        }
           

5 使用重入锁实现线程同步

在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。

ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。

ReenreantLock类的常用方法有:

ReentrantLock() : 创建一个ReentrantLock实例 
lock() : 获得锁 
unlock() : 释放锁 
           

例子:

class Bank {

            private int account = ;
            //需要声明这个锁
            private Lock lock = new ReentrantLock();
            public int getAccount() {
                return account;
            }
            //这里不再需要synchronized 
            public void save(int money) {
                lock.lock();
                try{
                    account += money;
                }finally{
                    lock.unlock();
                }

            }
        }
           

注意:

a.如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码

b.如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁

6 使用局部变量实现线程同步

如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

ThreadLocal 类的常用方法

详解见(https://blog.csdn.net/u013735511/article/details/70416597)

ThreadLocal() : 创建一个线程本地变量 
get() : 返回此线程局部变量的当前线程副本中的值 
initialValue() : 返回此线程局部变量的当前线程的"初始值" 
set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
           

注意:

a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题。

b.前者采用以”空间换时间”的方法,后者采用以”时间换空间”的方式

需要一个应用来理解

7 使用阻塞队列实现线程同步

关于队列,阻塞队列的问题!此处需要研究

8 使用原子变量实现线程同步

原子队列不慎理解!?