欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JAVA多线程——实现同步

程序员文章站 2022-03-08 18:58:28
...

转载(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=0;i<10;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=0;i<10;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=0;k<5;k++){
    System.out.println(Thread.currentThread().getName()+ " : for loop : " + k);
  }

synchronized(this){
  for(int k=0;k<5;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 : 0

t1_name : for loop : 1

t1_name : for loop : 2

t2_name : for loop : 0

t1_name : for loop : 3

t2_name : for loop : 1

t1_name : for loop : 4

t2_name : for loop : 2

t1_name : synchronized for loop : 0

t2_name : for loop : 3

t1_name : synchronized for loop : 1

t2_name : for loop : 4

t1_name : synchronized for loop : 2

t1_name : synchronized for loop : 3

t1_name : synchronized for loop : 4

t2_name : synchronized for loop : 0

t2_name : synchronized for loop : 1

t2_name : synchronized for loop : 2

t2_name : synchronized for loop : 3

t2_name : synchronized for loop : 4

第一个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(2000);  
                        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=0;  

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

}  

输出结果:

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(2000);  
                        System.out.println("thread3 really release lock");  
                    }  

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


    }  

}  

class Sum{  
    public Integer total=0;  

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

}  

输出:

thread1 get lock  
thread2 get lock  
thread3 get lock  
thread3 really release lock  
100  
thread2 release lock  
100  
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来执行,以确保条件满足和一定执行。如下代码:

1 public class K {
 2     //状态锁
 3     private Object lock;
 4     //条件变量
 5     private int now,need;
 6     public void produce(int num){
 7         //同步
 8         synchronized (lock){
 9            //当前有的不满足需要,进行等待
10             while(now < need){
11                 try {
12                     //等待阻塞
13                     wait();
14                 } catch (InterruptedException e) {
15                     e.printStackTrace();
16                 }
17                 System.out.println("我被唤醒了!");
18             }
19            // 做其他的事情
20         }
21     }
22 }
23             

此代码为一模板,具体实现看下面的例子
生产者消费者的问题,来源于(https://www.cnblogs.com/moongeek/p/7631447.html)
基本思想:假设有一个公共的容量有限的池子,有两种人,一种是生产者,另一种是消费者。需要满足如下条件:

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

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

1 import java.util.LinkedList;
 2 
 3 /**
 4  *  生产者和消费者的问题
 5  *  wait、notify/notifyAll() 实现
 6  */
 7 public class Storage1 implements AbstractStorage {
 8     //仓库最大容量
 9     private final int MAX_SIZE = 100;
10     //仓库存储的载体
11     private LinkedList list = new LinkedList();
12 
13     //生产产品
14     public void produce(int num){
15         //同步
16         synchronized (list){
17             //仓库剩余的容量不足以存放即将要生产的数量,暂停生产
18             while(list.size()+num > MAX_SIZE){
19                 System.out.println("【要生产的产品数量】:" + num + "\t【库存量】:"
20                         + list.size() + "\t暂时不能执行生产任务!");
21 
22                 try {
23                     //条件不满足,生产阻塞
24                     list.wait();
25                 } catch (InterruptedException e) {
26                     e.printStackTrace();
27                 }
28             }
29 
30             for(int i=0;i<num;i++){
31                 list.add(new Object());
32             }
33 
34             System.out.println("【已经生产产品数】:" + num + "\t【现仓储量为】:" + list.size());
35 
36             list.notifyAll();
37         }
38     }
39 
40     //消费产品
41     public void consume(int num){
42         synchronized (list){
43 
44             //不满足消费条件
45             while(num > list.size()){
46                 System.out.println("【要消费的产品数量】:" + num + "\t【库存量】:"
47                         + list.size() + "\t暂时不能执行生产任务!");
48 
49                 try {
50                     list.wait();
51                 } catch (InterruptedException e) {
52                     e.printStackTrace();
53                 }
54             }
55 
56             //消费条件满足,开始消费
57             for(int i=0;i<num;i++){
58                 list.remove();
59             }
60 
61             System.out.println("【已经消费产品数】:" + num + "\t【现仓储量为】:" + list.size());
62 
63             list.notifyAll();
64         }
65     }
66 }

代码解释:

生产的时候,正常生产(容量足够),不进入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 = 100;

            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 = 100;
            //需要声明这个锁
            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 使用原子变量实现线程同步
原子队列不慎理解!?