线程、多线程、线程安全等入门指南

简介: 线程、多线程、线程安全等入门指南

基础

概念

进程、线程、并发、并行

  • 进程:进程是资源分配的最小单位,是进入到内存中的程序
  • 线程:线程是CPU调度的最小单位,是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程(单线程程序),一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序
  • 并发:交替执行
  • 并行:同时执行

线程的调度

  • 分时调度

    所有的线程轮流使用 cpu 的使用权,平均分配每个线程占用 cpu 的时间

  • 抢占式调度

    优先让优先级高的线程使用 cpu,如果线程的优先级相同的,那么会随机选择一个线程执行(线程的随机性),java 使用的就是抢占式调度


线程的状态

  • 新建(NEW):至今尚未启动的线程处于这种状态
  • 运行(RUNNABLE):正在 Java 虚拟机中执行的线程处于这种状态
  • 阻塞(BLOCKED):受阻塞并等待某个监视器锁的线程处于这种状态
  • 无限等待(WAITING):无限期地等待另一个线程来执行某一特定操作的线程处于这种状态
  • 睡眠(计时等待)(TIMED_WAITING):等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态
  • 死亡(TERMINATED):已退出的线程处于这种状态

1594006149497.png

Object类中等待与唤醒线程的方法:

java.lang.Object 类:是祖宗类,里边的方法,任意的一个类都可以使用

void wait()         // 让当前进程等待。可以在其他线程调用此对象的 notify() 方法或 notifyAll() 方法唤醒当前进程
void notify()         // 唤醒在此对象监视器(同步锁,对象锁)上等待的单个线程。 
void notifyAll()     // 唤醒在此对象监视器(同步锁,对象锁)上等待的所有线程。 
/*注意:
    1.wait和notify方法一般使用在同步代码块中 ==> 有锁对象 ==> 对象监视器
    2.一般都使用锁对象调用wait和notify方法(多个线程使用的是同一个锁对象)
        Thread-0线程使用锁对象 ==> wait方法 ==> Thread-0线程进入到等待
        Thread-1线程使用锁对象 ==> notify方法 ==> 唤醒在锁对象上等待的Thread-0线程
    3.在同步中的线程调用wait方法,进入到等待状态,会释放锁对象
      在同步中的线程调用sleep方法,进入睡眠,不会释放锁对象
*/


实现多线程的方式

主要有继承 Thread 类实现 Runnable 接口两种方式。不过实际开发中一般都是通过线程池方式来获取空闲线程。

  • 实现 Runnable 接口(推荐)

    java.lang.Thread类 implements Runnable接口

    Thread类的构造方法:

    Thread(Runnable target) //传递Runnable接口的实现类对象
    Thread(Runnable target, String name) //传递Runnable接口的实现类对象和线程名称

    实现步骤:

    ​ 1.创建一个实现类,实现 Runnable 接口

    ​ 2.在实现类中重写 Runnable 接口中的 run 方法(设置线程任务)

    ​ 3.创建 Runnable 接口的实现类对象

    ​ 4.创建 Thread 类对象,在构造方法中传递 Runnable 接口的实现类对象

    ​ 5.调用 Thread 类中的 start() 方法,开启新的线程,执行 run 方法

  • 继承 Thread 类

    Thread 类本质上是实现了 Runnable 接口的一个实例;启动线程的唯一方法就是通过 Thread 类的 start() 实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。

    实现步骤:

    ​ 1.创建一个类继承 Thread 类

    ​ 2.在 Thread 类的子类中,重写 Thread 类中的 run 方法(设置线程任务)

    ​ 3.创建 Thread 类的子类对象

    ​ 4.调用继承自 Thread 类中的 start() 方法,开启新的线程执行 run 方法


两种方式的比较

  • 使用实现 Runnable 接口的方式创建多线程程序,可以避免单继承的局限性

    • 类继承了 Thread 类,就不能继承其他的类了
    • 类实现了 Runnable 接口,还可以继承其他的类
  • 使用实现 Runnable 接口的方式创建多线程程序,把设置线程任务和开启线程进行了解耦(解除了耦合性,增强扩展性)

    • 类继承了 Thread 类,在run方法设置什么任务,创建子类对象就只能执行什么任务(耦合性强)
    • 类实现 Runnable 接口目的:重写 run 方法设置线程任务

      创建Thread类对象的目的:传递不同的Runnable接口的实现类对象(传递不同的任务),执行不同的任务


Thread类常用方法

// 返回对当前正在执行的线程对象的引用
static Thread currentThread()
    
// 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),让线程睡眠,到时间睡醒了,继续执行
static void sleep(long millis)
    
// 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
void start()
/* 当我们调用start方法执行,结果是两个线程并发地运行;
     当前线程(main线程:运行main方法中的代码)和另一个线程(开启的新的线程:执行其 run 方法中的代码)。
     两个线程会一起抢夺cpu的执行权,谁抢到了谁执行,会出现随机性打印结果*/
// 注意;多次启动一个线程是非法的(一个线程对象只能调用一次start方法)。特别是当线程已经结束执行后,不能再重新启动。

// 返回该线程的名称        // 注意:getName方法只能在Thread类的子类中使用
String getName() 
  
// 设置线程名称
void setName(String name)


锁的相关概念

可重入锁

如果锁具备可重入性,则称作为可重入锁。像 synchronized 和 ReentrantLock 都是可重入锁,可重入性实际上表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。

  • 举个简单的例子,当一个线程执行到某个 synchronized 方法(method1)时,在 method1 中会调用另外一个 synchronized 方法 method2 ,此时线程不必重新去申请锁,而是可以直接执行方法 method2。

    class  MyClass {
         public synchronized void method1() {
             method2();
         }
         
         public synchronized void method2() {}
    }

    上述代码中的两个方法 method1 和 method2 都用 synchronized 修饰了,假如某一时刻,线程 A 执行到了 method1 ,此时线程A获取了这个对象的锁,而由于 method2 也是 synchronized 方法,假如 synchronized 不具备可重入性,此时线程 A 需要重新申请锁。但是这就会造成一个问题,因为线程A已经持有了该对象的锁,而又在申请获取该对象的锁,这样就会线程A一直等待永远不会获取到的锁。

    由于 synchronized 和 Lock 都具备可重入性,所以不会发生上述现象。


可中断锁

可中断锁:就是可以响应中断的锁。

在 Java 中,synchronized 就不是可中断锁,而 Lock 是可中断锁。

如果某一线程 A 正在执行锁中的代码,另一线程 B 正在等待获取该锁,可能由于等待时间过长,线程 B 不想等待了,想先处理其他事情,可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。


公平锁

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。

非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

  • Java 中的 synchronized 就是非公平锁,它无法保证等待的线程获取锁的顺序。
  • ReentrantLock 和 ReentrantReadWriteLock 默认情况下是非公平锁,但是可以设置为公平锁。

    在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。

    可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性:

    ReentrantLock lock =  new  ReentrantLock( true );
    // 如果参数为true表示为公平锁,为fasle为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。

    ReentrantLock 类的常用方法:

    // 判断锁是否是公平锁
    boolean isFair()
    // 判断锁是否被任何线程获取了
    boolean isLocked() 
    // 判断锁是否被当前线程获取了
    boolean isHeldByCurrentThread() 
    // 判断是否有线程在等待该锁
    boolean hasQueuedThreads() 

    ReentrantReadWriteLock 类中也有类似的方法,同样也可以设置为公平锁和非公平锁。不过注意:ReentrantReadWriteLock 并未实现 Lock 接口,它实现的是 ReadWriteLock 接口。


读写锁

读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。

因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。

ReadWriteLock 就是读写锁,它是一个接口,ReentrantReadWriteLock 实现了这个接口。

可以通过 readLock() 获取读锁,通过 writeLock() 获取写锁。


死锁

概念:线程获取不到锁对象,从而进不去同步中执行

前提:

  • 必须出现同步代码块嵌套
  • 必须有两个线程
  • 必须有两个锁对象

原因:

  • 出了两个同步代码块的嵌套
  • 线程一拿着线程二的锁,线程二拿着线程一的锁
  • 两个线程都处于阻塞状态,都不会继续执行


线程安全

高并发的线程安全问题

  • 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值

    拓展:所有的共享变量(成员变量、静态成员变量)都存储于主内存。每一个线程还存在自己的工作内存,线程的工作内存保留了被线程使用的变量的工作副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问

  • 有序性:即程序执行的顺序按照代码的先后顺序执行
  • 原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行


volatile 关键字

volatile 关键字的作用:解决变量的可见性、有序性,但不能解决变量的原子性

  • 解决可见性:一个成员变量被 volatile 关键字修饰,当改变该变量的值后,volatile 关键字会让该变量所有的变量副本立即失效,每个线程的工作内存想要使用该变量的值,需要在主内存中重新获取
  • 解决有序性:变量添加了volatile关键字,编译器就不会再对该变量的相关代码进行重排了


synchronized 关键字

synchronized 是 java 中的一个关键字,也就是说是 Java 语言内置的特性。

如果一个代码块被 synchronized 修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

  • 获取锁的线程执行完了该代码块,然后线程释放对锁的占有
  • 线程执行发生异常,此时JVM会让线程自动释放锁

如果这个获取锁的线程由于要等待 IO 或者其他原因(比如调用 sleep 方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,会非常影响程序的执行效率。


使用方式:

  • 方式1(方法内同步代码块):

    synchronized(锁对象){
         访问了共享数据的代码(产生了线程安全问题的代码)
    }
    // 注意:1.锁对象可以是任意的对象:new Object(); new Person(); "aaa"==>字符串底层是一个字符是数组,也是一个对象
    //       2.必须保证所有的线程使用的都是同一个锁对象
  • 方式2(同步方法)

    权限修饰符 synchronized 返回值类型 方法名(参数列表){
                访问了共享数据的代码(可能产生线程安全问题的代码)
    }
    // 注意:1.静态的同步方法的锁对象是本类的class文件对象
    //        2.非静态同步方法的锁对象是本类创建的对象,即 this


并发包

包路径:java.util.concurrent

主要内容:

  1. 并发集合类(CopyOnWriteArrayListCopyOnWriteArraySetConcurrentHashMap) 底层是CAS机制 乐观锁
  2. 原子类操作(AtomInteger、AtomicLong、AtomicBoolean) 底层是CAS(比较并替换)机制 乐观锁
  3. 锁操作(Lock、ReadWriteLock)
  4. 线程池(Callable、Future、Executor)
  5. 信号量(CountDownLatch、CyclicBarrier、Semapherre、Exchanger)


Hashtable和 ConcurrentHashMap 有什么区别:

  • Hashtable:采用的 synchronized ——悲观锁,效率低

    Hashtable 容器使用 synchronized 来保证线程安全,但在线程竞争激烈的情况下 Hashtable 的效率非常低下。

    因为其锁定的是整个哈希表,一个操作正在进行时,其他操作也同时锁定;当一个线程正在访问 Hashtable 的同步方法,其他线程也访问 Hashtable 的同步方法时,会进入阻塞状态;例如线程1使用 put 进行元素添加,线程2不但不能使用 put 方法添加元素,也不能使用 get 方法来获取元素,所以竞争越激烈效率越低。

    Hashtable 是 Java 类库中从 1.0 版本提供的一个线程安全的 Map,但已过时

  • ConcurrentHashMap:采用 CAS + 局部(synchronized)锁定

    CAS机制——乐观锁,效率更高。

    局部(synchronized)锁定:只锁定 “桶” 。仅对当前元素锁定,其他 ”桶“ 里的元素不锁定。


原子类

概述:java 从 JDK1.5 开始提供了 java.util.concurrent.atomic 包(简称 Atomic 包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。

属于乐观锁,只能解决一个变量的原子性,底层是 CAS 机制,反复比较,只有内存中的值和预期的值一样,才会进行修改,否则就会循环重新获取值。

常用原子类:AtomicInteger、AtomicLong、AtomicBoolean、AtomicIntegerArray

  • AtomicInteger 原子型 Integer,可以实现原子更新操作
// 构造方法:
public AtomicInteger():                       // 初始化一个默认值为0的原子型Integer
public AtomicInteger(int initialValue)     // 初始化一个指定值的原子型Integer

// 成员方法:
int get():                        // 获取值
int getAndIncrement():          // 以原子方式将当前值加1。注意,这里返回的是自增前的值。
int incrementAndGet():             // 以原子方式将当前值加1。注意,这里返回的是自增后的值。
int addAndGet(int data):        // 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。
int getAndSet(int value):       // 以原子方式设置为newValue的值,并返回旧值。
  • AtomicIntegerArray:可以用原子方式更新其元素的 int 数组:可以保证数组的原子性
// 构造方法
AtomicIntegerArray(int length)         // 创建指定长度的给定长度的新 AtomicIntegerArray
AtomicIntegerArray(int[] array)     // 创建与给定数组具有相同长度的新 AtomicIntegerArray,并从给定数组复制其所有元素
    
// 成员方法:
int addAndGet(int i, int delta)     // 以原子方式将给定值与索引 i 的元素相加
    // i:获取指定索引处的元素
    // delta:给元素增加的值

int get(int i)         // 获取指定索引处元素的值


Lock 锁

参考:Lock接口常用方法

常用的 Lock 锁:

  • ReentrantLock

    java.util.concurrent.locks.ReentrantLock类 implements Lock接口

格式:

ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
     //处理任务
} catch (Exception ex){
     
} finally {
     lock.unlock();    //释放锁
}

if (lock.tryLock()) {
      try {
          //处理任务
      } catch (Exception ex){
         
      } finally {
          lock.unlock();    //释放锁
      } 
} else  {
     //如果不能获取锁,则直接做其他事情
}

常用方法:

// 获取锁(获取不到锁时会一直尝试获取锁,不能中断)
void lock();
// 释放锁
void unlock();
// 尝试获取锁(获取成功,返回 true,否则返回 false,不会阻塞;常用if语句判断 tryLock() 的返回结果)
boolean tryLock();
// 尝试获取锁(在指定时间内获取到锁,返回 true;在指定时间内未获取到锁,返回 false,不会阻塞,并且可以响应中断)
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 可响应中断的获取锁(获取不到锁时会一直尝试获取锁,线程可以使用 interrupt()方法进行响应中断)
void  lockInterruptibly()  throws  InterruptedException;


信号量

  • CountDownLatch:多线程协作。允许一个或多个线程等待其他线程完成操作
  • CyclicBarrier :多线程协作。让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行
  • Semaphore :并发数量控制。主要作用是控制线程的并发数量
  • Exchanger :线程信息交互。进行线程间的数据交换


CountDownLatch

用给定的计数初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。

CountDownLatch 是通过一个计数器来实现的,每当一个线程完成了自己的任务后,可以调用countDown()方法让计数器 -1,当计数器到达 0 时,调用 CountDownLatch 的 await() 方法的线程阻塞状态解除,继续执行。

// 构造方法:
CountDownLatch(int count)     //构造一个用给定计数初始化的 CountDownLatch
    // 参数:
    // int count:传递计数器的初始化数字

// 成员方法:
void await()          // 在内部的计数器值清零之前,会一直让线程等待
void countDown()    // 让内部计数器的值减1

// 注意:
//    必须保证多个线程使用的是同一个CountDownLatch对象
//    在多个线程类中定义CountDownLatch变量,使用带参构造方法给CountDownLatch变量赋值

示例:

// 线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后,要线程2打印B之后才能打印C,所以:线程1在打印A后,必须等待线程2打印完B之后才能继续执行打印C。

public class MyThreadAC extends  Thread{
    //定义成员变量CountDownLatch
    private CountDownLatch countDownLatch;

    //使用带参数构造方法给CountDownLatch变量赋值
    public MyThreadAC(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        System.out.println("A");

        //使用CountDownLatch对象中的方法await,让线程等待,等待CountDownLatch对象内部计数器的值变成0在执行
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("C");
    }
}

public class MyThreadB extends Thread {
    //定义成员变量CountDownLatch
    private CountDownLatch countDownLatch;

    //使用带参数构造方法给CountDownLatch变量赋值
    public MyThreadB(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        System.out.println("B");
        //使用CountDownLatch对象中的方法countDown让计数器的值-1
        countDownLatch.countDown();
    }
}

public class Demo01Thread {
    public static void main(String[] args) throws InterruptedException {
        //创建CountDownLatch对象,分别传递给每一个线程使用
        CountDownLatch cdl = new CountDownLatch(1);//创建内部计数器的值为1

        new MyThreadAC(cdl).start();
        Thread.sleep(1000);//睡眠1秒钟,保证AC线程先执行
        new MyThreadB(cdl).start();
    }
}


CyclicBarrier

一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点。即设置屏障,一个线程等待其他多个线程全部执行完毕,再执行

使用场景:CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的场景。

需求:使用两个线程读取2个文件中的数据,当两个文件中的数据都读取完毕以后,进行数据的汇总操作。

构造方法:

// 创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。
CyclicBarrier(int parties, Runnable barrierAction)
    // 参数:
    //         int parties:设置的屏障的数量,设置的线程数量,设置几个线程执行完,再让其他的线程执行
    //        Runnable barrierAction:达到屏障之后执行的线程

常用方法:

int await()        // 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞

示例:

// 公司召集5名员工开会,等5名员工都到了,会议开始。
// 创建5个员工线程,1个开会线程,几乎同时启动,使用CyclicBarrier保证5名员工线程全部执行后,再执行开会线程。

// 员工线程
public class PersonThread extends Thread {
    //定义一个成员变量CyclicBarrier
    private CyclicBarrier cyclicBarrier;

    //使用带参数构造方法给CyclicBarrier变量赋值
    public PersonThread(CyclicBarrier cyclicBarrier) {
        this.cyclicBarrier = cyclicBarrier;
    }

    @Override
    public void run() {
        try {
            int r = (int)(Math.random()*1000);//获取一些随机的数字
            Thread.sleep(r);
            System.out.println(Thread.currentThread().getName()+"线程花了"+r+"毫秒来到了会议的现场!");
            cyclicBarrier.await();//把CyclicBarrier内部的屏障数-1
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

// 开会线程
public class MeetingThread extends Thread {
    @Override
    public void run() {
        System.out.println("人齐了,开始开会了!");
    }
}

public class Demo01CyclicBarrier {
    public static void main(String[] args) {
        //创建CyclicBarrier对象,屏障数的值5,当屏障数的值变成0,就会执行参数传递的线程对象的run方法
        CyclicBarrier cb = new CyclicBarrier(5,new MeetingThread());

        //创建5个人的线程,并开启线程
        PersonThread p1 = new PersonThread(cb);
        PersonThread p2 = new PersonThread(cb);
        PersonThread p3 = new PersonThread(cb);
        PersonThread p4 = new PersonThread(cb);
        PersonThread p5 = new PersonThread(cb);
        p1.start();
        p2.start();
        p3.start();
        p4.start();
        p5.start();
    }
}

// 执行结果:
/*
Thread-4线程花了192毫秒来到了会议的现场!
Thread-3线程花了594毫秒来到了会议的现场!
Thread-2线程花了616毫秒来到了会议的现场!
Thread-5线程花了809毫秒来到了会议的现场!
Thread-1线程花了884毫秒来到了会议的现场!
人齐了,开始开会了!
*/


Semaphore

主要作用是控制线程的并发数量,设置同时允许几个线程执行。

构造方法:

public Semaphore(int permits)                
public Semaphore(int permits, boolean fair)    
//    参数:
//        permits 表示许可线程的数量
//         fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程

常用方法:

void acquire()     // 表示获取许可  lock(获取锁对象)
void release()     // 表示释放许可  unlock(归还锁对象)

示例:同时允许2个线程同时执行

// 教室的线程类
public class ClassRoom {
    //创建Semaphore对象,参数传递2,表示可以允许2个线程获取许可执行
    Semaphore semaphore = new Semaphore(2);

    //定义线程进入到教室参观的方法
    public void intoClassRoom() throws InterruptedException {
        semaphore.acquire();//表示获取许可,允许2个线程获取许可执行
            System.out.println(Thread.currentThread().getName()+"...进入到教室参观!");
            Thread.sleep(2000);//让线程在教室参观2秒钟
            System.out.println(Thread.currentThread().getName()+"...离开了教室!");
        semaphore.release();//表示释放许可
    }
}

//学生的线程类
public class StudentThread extends Thread {
    //定义一个教室变量
    private ClassRoom classRoom;
    //使用带参数构造方法给ClassRoom变量赋值
    public StudentThread(ClassRoom classRoom) {
        this.classRoom = classRoom;
    }

    @Override
    public void run() {
        try {
            //调用ClassRoom里边参观的方法
            classRoom.intoClassRoom();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 测试类
public class Demo01Semaphore {
    public static void main(String[] args) {
        //创建一个教室
        ClassRoom cr = new ClassRoom();
        //创建5名学生对象,进入到教室参观
        for (int i = 0; i < 5; i++) {
            new StudentThread(cr).start();
        }
    }
}

// 执行结果:
/*
Thread-1...进入到教室参观!
Thread-0...进入到教室参观!
Thread-0...离开了教室!
Thread-2...进入到教室参观!
Thread-1...离开了教室!
Thread-3...进入到教室参观!
Thread-2...离开了教室!
Thread-4...进入到教室参观!
Thread-3...离开了教室!
Thread-4...离开了教室!
*/


Exchanger

Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger 用于进行线程间的数据交换。

两个线程通过 exchange 方法交换数据,如果第一个线程先执行 exchange() 方法,它会一直等待第二个线程也执行 exchange 方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。

注意:

  • 保证两个交互的线程使用的是同一个 Exchanger 对象,定义成员变量,使用构造方法赋值
  • exchange 具有阻塞特性,等待对方线程

使用场景:

  • 可以做数据校对工作

    比如需要将纸制银行流水通过人工的方式录入成电子银行流水。为了避免错误,采用 AB 岗两人进行录入,录入到两个文件中,系统需要加载这两个文件,并对两个文件数据进行校对,看看是否录入一致

构造方法:

public Exchanger()

常用方法:

V exchange(V x)        // 参数传递给对方的数据,返回值接收对方返回的数据
V exchange(V x, long timeout, TimeUnit unit)
// 参数:
//        long timeout:设置等待的时长。超时抛出异常,结束等待
//        TimeUnit unit:设置等待的时间单位(秒,分钟,小时,天)

示例:

public class ThreadA extends Thread {
    private Exchanger<String> exchanger;

    public ThreadA(Exchanger<String> exchanger) {
        this.exchanger = exchanger;
    }

    @Override
    public void run() {
        System.out.println("线程A开始执行");
        System.out.println("线程A给线程B100元,并从线程B得到一张火车票!");
        String result = null;
        try {
            result = exchanger.exchange("100元");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程A得到的东西:"+result);
    }
}

public class ThreadB extends Thread{
    private Exchanger<String> exchanger;

    public ThreadB(Exchanger<String> exchanger) {
        this.exchanger = exchanger;
    }

    @Override
    public void run() {
        System.out.println("线程B开始执行");
        System.out.println("线程B给线程A一张火车票,并从线程A得到100元!");
        String result = null;
        try {
            result = exchanger.exchange("一张火车票");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程B得到的东西:"+result);
    }
}

public class Demo01Exchanger {
    public static void main(String[] args) {
        //创建Exchanger对象
        Exchanger<String> exchanger = new Exchanger<>();
        //创建线程A对象,并运行
        new ThreadA(exchanger).start();
        new ThreadB(exchanger).start();
    }
}

// 执行结果:
/*
线程A开始执行
线程A给线程B100元,并从线程B得到一张火车票!
线程B开始执行
线程B给线程A一张火车票,并从线程A得到100元!
线程B得到的东西:100元
线程A得到的东西:一张火车票
*/


常见的锁机制的异同

常见的锁机制有:

  • synchronized:java 关键字,属于悲观锁,通过在方法、类或代码块中加入该关键字给共享资源上锁,只有拿到锁的那个线程才可以访问共享资源,其他线程在拿到锁之前会阻塞
  • Atomic 原子类:属于乐观锁,只能解决一个变量的原子性,底层是CAS机制,反复比较,只有内存中的值和预期的值一样,才会进行修改,否则就会循环重新获取值
  • ReentrantLock:java类,属于悲观锁,在可能出现线程安全问题的代码前后进行加锁和解锁操作,只让一个线程进入到两个方法中间执行
  • Semaphore:基本能完成 ReentrantLock 的所有工作,使用方法也与之类似,但它可以设置同时允许几个线程执行


volatile 关键字和 synchronized 关键字的区别:

  • volatile 关键字:只能修饰变量,可以解决变量的可见性,有序性,不能解决原子性
  • synchronized 关键字:不能修饰变量,可以修饰方法,代码块。可以解决:可见性,有序性,原子性


synchronized 和 lock 锁的区别

  • Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现
  • synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生

    Lock 在发生异常时,若没有主动调用 unLock() 去释放锁,则很可能造成死锁现象,故使用 Lock 时需要在 finally 块中释放锁

  • Lock 可以让等待锁的线程响应中断

    synchronized 不能够响应中断,等待的线程会一直等待下去

  • Lock 可以返回有没有成功获取锁,而 synchronized 却无法办到
  • Lock 可以提高多个线程进行读操作的效率
  • 性能上,如果竞争资源不激烈,两者的性能是差不多的;当竞争资源非常激烈时(即高并发时),Lock 的性能要远远优于 synchronized,在具体使用时要根据适当情况选择。

    因为 synchronized 是悲观锁,同一时间只能有一个线程获取到锁,而 Lock 提供了多样化的同步,比如有时间限制的同步,超时可以中断同步,比如使用读写锁(ReadWriteLock ),可以实现读写分离,提高多个线程进行读操作的效率。


CAS(乐观锁) 和 Synchronized(悲观锁)的区别:

  • 悲观锁 是从悲观的角度出发:总是假设最坏的情况,每次去拿数据的时候都认为其他线程会修改,所以每次在拿数据的时候都会上锁,这样其他线程要拿这个数据就会阻塞直到它拿到锁。

    共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

    Synchronized 和 ReentrantLock 都是悲观锁

  • CAS(乐观锁)是从乐观的角度出发:总是假设最好的情况,每次去拿数据的时候都认为其他线程不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间其他线程有没有去更新这个数据。


Threadlocal

Threadlocal

  • 将数据绑定到当前线程上,同一个线程中。同一线程中经过的不同方法都可以从 Threadlocal 获取数据,并且获取的数据是同一个对象。

    即 ThreadLocal 提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过 get 和 set 方法就可以得到当前线程对应的值。

Threadlocal 使用的方法:

ThreadLocal<T> sThreadLocal = new ThreadLocal<T>();
sThreadLocal.set(t)     // 将数据绑定到当前线程
sThreadLocal.get()         // 从当前线程中获取数据

示例:

// 事务管理器
@Component
public class TxManager {

    @Autowired
    private DataSource dataSource;

    // 准备好本地存储Connection对象的ThreadLocal
    private ThreadLocal<Connection> th = new ThreadLocal<Connection>();

    // 获取Connection对象
    public Connection getConnection() throws SQLException {
        Connection connection = th.get();
        if (connection == null){
            connection = dataSource.getConnection();
            th.set(connection);
        }
        return connection;
    }
}
相关文章
|
4天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
14天前
|
安全 Java 调度
Java中的多线程编程入门
【10月更文挑战第29天】在Java的世界中,多线程就像是一场精心编排的交响乐。每个线程都是乐团中的一个乐手,他们各自演奏着自己的部分,却又和谐地共同完成整场演出。本文将带你走进Java多线程的世界,让你从零基础到能够编写基本的多线程程序。
29 1
|
20天前
|
Java 数据处理 开发者
Java多线程编程的艺术:从入门到精通####
【10月更文挑战第21天】 本文将深入探讨Java多线程编程的核心概念,通过生动实例和实用技巧,引导读者从基础认知迈向高效并发编程的殿堂。我们将一起揭开线程管理的神秘面纱,掌握同步机制的精髓,并学习如何在实际项目中灵活运用这些知识,以提升应用性能与响应速度。 ####
43 3
|
22天前
|
Java
Java中的多线程编程:从入门到精通
本文将带你深入了解Java中的多线程编程。我们将从基础概念开始,逐步深入探讨线程的创建、启动、同步和通信等关键知识点。通过阅读本文,你将能够掌握Java多线程编程的基本技能,为进一步学习和应用打下坚实的基础。
|
23天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
16 3
|
23天前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
16 2
|
23天前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
28 2
|
23天前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
28 1
|
23天前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
33 1
|
23天前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
24 1