由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问造成的这种问题。
由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized 方法和 synchronized 块。

1.synchronized 方法

  1. 通过在方法声明中加入 synchronized关键字来声明,语法如下:
public  synchronized  void accessVal(int newVal);
  synchronized 方法控制对“对象的类成员变量”的访问:每个对象对应一把锁,每个 synchronized 方法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。

2.synchronized块

  synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。<br />      Java 为我们提供了更好的解决办法,那就是 synchronized 块。 块可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率。<br />      synchronized 块:通过 synchronized关键字来声明synchronized 块,语法如下:
synchronized(syncObject)
   { 
   //允许访问控制的代码 
   }

【示例】多线程操作同一个对象(使用线程同步)

package Thread;

/**
 * 线程安全, 在并发时保证数据的正确性, 效率尽可能高
 * synchronized
 */
public class SynTest01 {

    public static void main(String[] args) {
        SafeWeb12306 web = new SafeWeb12306();
        new Thread(web,"码畜").start();
        new Thread(web,"码农").start();
        new Thread(web,"蚂蟥").start();

    }
}

class SafeWeb12306 implements Runnable{
    private int ticketNums = 99;
    private boolean flag = true;

    @Override
    public void run() {
        while (flag){
            try{
                Thread.sleep(100);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            test();
        }
    }
    public synchronized void test(){
            if (ticketNums <= 0){
                flag = false;
                return;
            }

            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "-->" + ticketNums);
            ticketNums--;
    }
}

3.线程协作—生产者/消费者模式

1.管程法

解决方式1, 并发协作模型”生产者/消费者模式” —> 管程法

  • 生产者: 负责生产数据的模块(这里模块可能是:方法\对象\线程\进程)
  • 消费者: 负责处理数据的模块(这里模块可能是:方法\对象\线程\进程)
  • 缓冲区: 消费者不能直接使用生产者的数据, 他们之间有个”缓冲区”;

生产者将生产好的数据放入”缓冲区”, 消费者从”缓冲区”拿要处理的数据.
image.png

示例

package Thread;


/**
 * 协作模型, 生产者消费者实现方式1, 管程法
 */

public class CoTest01 {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        Productor p = new Productor(container);
        Consumer c = new Consumer(container);
        p.start();
        c.start();


    }
}

//生产者
class Productor extends Thread{
    SynContainer container;
    public Productor(SynContainer container){
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            container.push(new Steamedbun(i));
            System.out.println("生产-->"+i+"个馒头!");
        }

    }
}
//消费者
class Consumer extends Thread{
    SynContainer container;

    public Consumer(SynContainer container){
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费-->"+container.pop().id+"个馒头!");
        }
    }
}

//缓冲区
class SynContainer{
    Steamedbun[] buns = new Steamedbun[10]; //存储容器
    int count = 0;//计数器
    //存储 生产
    public synchronized void push(Steamedbun bun){
        if (count == buns.length){  //何时能生成, 容器存在空间, 数据满了就等待
            try {
                this.wait(); //线程阻塞, 消费者通知生产解除
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        buns[count] = bun; //存在空间可以消费
        count ++;
        this.notifyAll();  //已生产,通知消费线程
    }
    //获取消费
    public synchronized Steamedbun pop(){
        if (count == 0) {   //何时消费, 容器中是否存在数据, 没有数据就等待
            try {
                this.wait(); //生产者通知, 解决等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        count--; //有数据可以消费
        Steamedbun bun = buns[count];
        this.notifyAll(); //已消费有空间, 通知等待的生产线程
        return bun;
    }
}

class Steamedbun{
    int id;
    public Steamedbun(int id){
        this.id = id;
    }

}

2.信号灯法

解决方式2, 并发协作模型”生产者/消费者模式” —> 信号灯法
image.png

示例

package Thread;

/**
 * 协作模型, 生产者消费者实现方式1, 信号灯法
 *  借助标志位
 */

public class CoTest02 {
    public static void main(String[] args) {
        Tv tv = new Tv();
        new Player(tv).start();
        new Watcher(tv).start();

    }
}


//生产者 演员
class Player extends Thread{
    Tv tv;
    public Player(Tv tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 20 == 0){
                this.tv.play("奇葩说");
            }else {
                this.tv.play("播放广告");
            }
        }
    }
}

//消费者 观众
class Watcher extends Thread{
    Tv tv;
    public Watcher(Tv tv){
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}

//同一个资源 电视
class Tv{
    String voice;
    //信号灯
    //T 表示演员表演,观众等待
    //F 表示观众观看,演员等待
    boolean flag = true;

    //表演
    public synchronized void play(String voice){
        //演员等待
        if (!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("表演了: " + voice);
        this.voice = voice;
        this.notifyAll();
        this.flag = !this.flag;
    }

    //观看
    public synchronized void watch(){
        //观众等待
        if (flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("听到了:"+voice);
        this.notifyAll();
        this.flag = !this.flag;
    }
}

4,等待唤醒机制

多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制
等待唤醒机制所涉及到的方法:

  1. wait方法

wait方法是Object类中定义的实例方法。
在指定对象上调用wait方法能够让当前线程进入阻塞状态(前提时当前线程持有该对象的内部锁(monitor)),
此时当前线程会释放已经获取的那个对象的内部锁,这样一来其他线程就可以获取这个对象的内部锁了。
当其他线程获取了这个对象的内部锁,进行了一些操作后可以调用notify方法来唤醒正在等待该对象的线程。

  1. notify/notifyAll方法

notify/notifyAll方法也是Object类中定义的实例方法。
它俩的作用是唤醒正在等待相应对象的线程,区别在于前者唤醒一个等待该对象的线程,而后者唤醒所有等待该对象的线程。
示例如上线程协作的两种方式

等待唤醒详细理解

为了支持多线程之间的协作,JDK提供了两个非常重要的方法:等待wait()方法和通知notify()方法。
这2个方法并不是在Thread类中的,而是在Object类中定义的。这意味着所有的对象都可以调用者两个方法

当在一个对象实例上调用wait()方法后,当前线程就会在这个对象上等待。
这是什么意思?比如在线程A中,调用了obj.wait()方法,那么线程A就会停止继续执行,转为等待状态。
等待到什么时候结束呢?
线程A会一直等到其他线程调用obj.notify()方法为止,这时,obj对象成为了多个线程之间的有效通信手段。

那么wait()方法和notify()方法是如何工作的呢?如图展示了两者的工作过程。
如果一个线程调用了object.wait()方法,那么它就会进出object对象的等待队列
这个队列中,可能会有多个线程,因为系统可能运行多个线程同时等待某一个对象。
当object.notify()方法被调用时,它就会从这个队列中随机选择一个线程,并将其唤醒。
image.png

Object.wait()方法并不能随便调用。它必须包含在对应的synchronize语句汇总,无论是wait()方法或者notify()方法都需要首先获取目标独享的一个监视器。
图中显示了wait()方法和nofiy()方法的工作流程细节。其中T1和T2表示两个线程。T1在正确执行wait()方法钱,必须获得object对象的监视器。
而wait()方法在执行后,会释放这个监视器。这样做的目的是使其他等待在object对象上的线程不至于因为T1的休眠而全部无法正常执行。

线程T2在notify()方法调用前,也必须获得object对象的监视器。所幸,此时T1已经释放了这个监视器,
因此,T2可以顺利获得object对象的监视器。
接着,T2执行了notify()方法尝试唤醒一个等待线程,
这里假设唤醒了T1。T1在被唤醒后,要做的第一件事并不是执行后续代码,而是要尝试重新获得object对象的监视器,而这个监视器也正是T1在wait()方法执行前所持有的那个。
如果暂时无法获得,则T1还必须等待这个监视器。当监视器顺利获得后,T1才可以在真正意义上继续执行
image.png


5.volatile

volatile保证线程间变量的可见性, 简单地说就是当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动, 更详细地说是要符合以下两个规格:

  • 线程对变量和进行修改之后, 要立刻回写到主内存
  • 线程对变量读取的时候, 要从主内存中读, 而不是缓存

image.png
各线程的工作内存间批次独立, 互不可见, 在线程启动的时候,虚拟机为每个内存分配一块工作内存, 不仅包含了线程内部定义的局部变量, 也包含了线程所需要使用的共享变量(非线程内构造的对象)的副本, 即为了提高执行效率
volatile是不错的机制, 但是volatile不能保证原子性

示例
双重检查锁-单例模式