JAVA多线程学习笔记(二):线程同步的方法以及线程间通讯

博主最近在学习高洪岩编写的《Java多线程编程核心技术》,之前有一篇:
JAVA多线程学习笔记(一): 多线程的基础概念以及Thread类常用方法介绍
这一篇主要整理的是线程同步的方法以及线程间通讯:

4 线程同步

4.1 线程不安全

4.1.1 现象&原因

  • 用线程访问的对象中如果有多个实例变量,则运行的结果有可能出现交叉的情况
  • 一个线程在操作共享数据的过程中,未执行完的情况下,另外的线程参与进来,导致共享数据出现安全问题

4.1.2 如何解决

  • 必须让一个线程操作完共享数据之后,才可以让另外的线程进入
  • java如何实现线程安全:线程同步

4.1.3 注意点

  • 方法内的变量:线程安全。各个线程私有的变量,不会互相访问到
  • 实例变量:非线程安全。如果多个线程共同访问1个对象中的实例变量,会出现线程不安全的现象

4.2 synchronized 关键字

4.2.1 简介

synchronized关键字在java用于实现线程同步,只要的方式有两种

  • 同步代码块:对象监视器为Object时使用
  • 同步方法:对象监视器为Class时使用

4.2.2 同步代码块

将操作同步数据的代码块,用synchronized括号包起来,即括号内为同步的代码块,将保证一个线程访问该部分代码时,其他方法在外等待直至此线程执行完该部分代码块

  • 共享数据:多个线程共同操作的同一个对象
  • 同步监视器:由 一个类的对象(object) 来充当,哪一个线程取此监视器,谁就可以执行同步代码块的内容。俗称:锁

同步代码块:

   synchronized(Object object/*同步监视器*/){
       // 需要被同步的代码块(即为操作共享数据的代码)
   }

注意点:

  • 必须共用同一把锁才能启动作用,当没有办法实现同步的时候,要去判断作为锁的对象是否时唯一的。如果每个线程都创建一个新的对象的,起不到任何作用
  • 对于继承thread实现的线程,注意同步监视器的对象是否需要加上static
  • 当一个线程访问object的一个synchroniezd同步代码块时,另一个线程仍可以访问该对象中的非synchronized(this)同步代码块
  • synchronized(this)代码块锁定的是当前对象

4.2.3 同步方法

将操作同步数据的方法,声明为synchronized,即此方法为同步方法,将保证一个线程访问该方法时,其他方法在外等待直至此线程执行完此方法

    // 在需要实现同步的方法加上关键字synchronized
    synchronized method(){
    }

注意点:

  • 同步不具有继承性,还需要在子类的方法中添加synchronized关键字
  • 对一个类的方法加上synchronized,加锁的时候,是对对象进行加锁,相当于,两个线程访问同一个对象的两个不同的同步方法,执行的结果是同步的(因此可以一个线程异步执行A对象的同步方法时,另外一个线程执行A对象的非同步方法)
  • 对一个static方法加上synchronized,加锁的时候,是给Class类加上锁,相当于实现了synchronized(class)的作用(即,锁定该类的所有实例)

4.3 volatile 关键字

4.3.1 作用

  • 主要作用是使得变量在多个线程之间可见,但volatile关键字最致命的缺点就是不支持原子性

4.3.2 原理

  • 通过使用volatile关键字,强制的从公共内存中读取变量的值
  • 但这里就涉及volatile一个致命的问题,它只保证了强制对数据的读写,但本身不处理数据的原子性
  • 举例:i++。非原子操作,分为:①从内存取出i ②计算i的值 ③将i的值写到内存中。如果在第二步出现修改,则会出现脏数据

4.3.3 场景

  • 在多个线程中可以感知实例变量被更改了,并且可以获得最新的值的使用,也就是用多线程读取共享变量时可以获得最新值

4.4 关键字synchronized & 关键字 volatile

  • volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰于变量,而synchronized可以修饰方法、代码块
  • 多线程访问volatile不会发生阻塞,synchronized会出现阻塞
  • volatile能保证数据的可见性,但不能保证原子性,而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公有内存中的数据做同步(因为synchronized保证在同一时间只有一个线程可以执行某一个方法或某一代码块,这包含了互斥性跟可见性)
  • 重点:volatile解决的是变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性

5. 线程间通信

5.1 while语句

  • 最原始最直接实现线程之间的通信机制,就是通过while语句来实现轮询机制来检测某一个条件。对应的缺点也很明显:轮询间隔小,则CPU资源的浪费,轮询间隔大,则不一定能得到想要的数据。

5.2 同步机制

  • 这里讲的同步是指多个线程通过 synchronized 关键字这种方式来实现线程间的通信
  • 这种方法的前提是:多个线程需要访问同一个共享变量。通信的方法:谁拿到了锁(获得了访问权限),谁就可以执行

5.3 等待/通知机制

  • 等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。

5.3.1 基础方法

  • wait():令当前线程挂起并放弃CPU、同步资源,使别的线程可访问并修改共享资源,而且当前线程排队等待再次对资源的访问
  • notify():随机唤醒正在排队等待同步资源的线程的一个线程结束等待
  • notifyAll():唤醒正在排队等待的所有线程结束等待(这里是优先级最高的线程先执行,还是随机执行,取决于JVM的实现)

5.3.2 注意点

  • Java.lang.Object提供的这三个方法,只有在synchronized方法或代码块中才能使用
  • 方法wait()释放锁,notify()不释放锁。即,线程实行wait()之后,将释放锁,而notify()执行的时候,唤醒其他线程,但并不释放锁。即唤醒的线程不一定马上就可以执行,要等调用notify()方法的线程释放锁粥,等待线程才有机会从wait()返回。
  • wait(long)方法的功能是:等待某一时间内是否有线程对锁进行唤醒,如果超过这个时间,则自动唤醒。

5.1.4 经典模型->生产者消费者

  • 一生产者一消费者
  • 一生产者多消费者
  • 多生产者一消费者
  • 多生产者多消费者

关于生产者消费者的一个代码样例:

import java.util.LinkedList;

public class Stroge {

    // 用于存放生产出来的产品
    private LinkedList<String> list;
    // 记录最大的容量
    private int maxSize;

    public Stroge(LinkedList<String> list, int maxSize) {
        this.list = list;
        this.maxSize = maxSize;
    }

    // 生产函数
    public void produce() {
        try {
            synchronized (list) {
                // 生产者可能被其他生产者唤醒,唤醒之后判断数目,如果无需生产,继续 wait()
                while (list.size() == maxSize) {
                    list.wait();
                }
                // 生产一个产品
                list.add(System.currentTimeMillis() + "");
                System.out.println(Thread.currentThread().getName() + " 生产一个产品,当前总数: " + this.list.size());
                // 唤醒其他的线程
                list.notifyAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 消费产品
    public String consume() {
        try {
            synchronized (list) {
                // 消费者可能被其他消费者唤醒,唤醒之后判断数目,没有可消费的,继续 wait()
                while (list.size() == 0) {
                    list.wait();
                }
                // 取走一个产品
                String consume = list.remove();
                System.out.println(Thread.currentThread().getName() + " 消费一个产品,当前总数: " + this.list.size());
                // 唤醒其他的线程
                list.notifyAll();
                return consume;
            }
        } catch (InterruptedException e) {
            return null;
        }
    }

    public static void main(String[] args) {
        LinkedList<String> linkedList = new LinkedList<>();
        Stroge stroge = new Stroge(linkedList, 10);
        // 下面创建两个消费者跟两个生产者
        Consumer consumer1 = new Consumer(stroge);
        Consumer consumer2 = new Consumer(stroge);
        Producer producer1 = new Producer(stroge);
        Producer producer2 = new Producer(stroge);
        Thread consumerThread1 = new Thread(consumer1);
        Thread consumerThread2 = new Thread(consumer2);
        Thread produceThread1 = new Thread(producer1);
        Thread produceThread2 = new Thread(producer2);
        consumerThread1.setName("消费者1");
        consumerThread2.setName("消费者2");
        produceThread1.setName("生产者1");
        produceThread2.setName("生产者2");
        consumerThread1.start();
        consumerThread2.start();
        produceThread1.start();
        produceThread2.start();
    }
}

class Producer implements Runnable {

    private Stroge stroge;

    public Producer(Stroge stroge) {
        this.stroge = stroge;
    }

    public void run() {
        while (true) {
            stroge.produce();
        }
    }

}

class Consumer implements Runnable {
    private Stroge stroge;

    public Consumer(Stroge stroge) {
        this.stroge = stroge;
    }

    public void run() {
        while (true) {
            stroge.consume();
        }
    }
}

5.4 管道

  • 通过管道进行线程间通信:字符流、字符流

5.5 join方法

  • 方法 join 的作用是使所属的线程对象 x 正常执行 run() 方法中的任务,而使当前线程 z 进行无限期的阻塞,等待线程 x 销毁后,再继续执行线程 z 后面的代码(即在A线程中调用了B线程的join方法,表示执行到这里,A停止,直到B线程执行完成,再继续执行A线程的剩余代码)

5.6 类ThreadLocal

  • ThreadLocal 是多线程中用于解决每个线程自己的共享变量的问题。可以理解为存储每个线程的私有数据
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值