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静态方法上持有的是对象锁。
这里写图片描述
线程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状态
这里写图片描述
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 使用原子变量实现线程同步
原子队列不慎理解!?

猜你喜欢

转载自blog.csdn.net/weixin_38719347/article/details/81219514