Java 多线程


学习链接

  1. 千锋教育——多线程
  2. 尚学堂——学习视频
  3. 尚学堂——学习笔记

一、进程、线程

1.1 进程

  1. 进程是程序的一次动态执行过程, 占用特定的地址空间。

  2. 每个进程由3部分组成:cpu、data、code

    每个进程都是独立的,保有自己的cpu时间,代码和数据,即便用同一份程序产生好几个进程,它们之间还是拥有自己的这3样东西,这样的缺点是:浪费内存,cpu的负担较重。

  3. 多任务(Multitasking)操作系统将CPU时间动态地划分给每个进程,操作系统同时执行多个进程,每个进程独立运行。以进程的观点来看,它会以为自己独占CPU的使用权。

  4. 进程的查看

    • Windows系统: Ctrl+Alt+Del,启动任务管理器即可查看所有进程。
    • Unix系统: ps or top。

1.2 线程

  1. 一个进程内部的一个执行单元,线程是程序中的一个单一的顺序控制流程

  2. 一个进程可拥有多个并行的(concurrent)线程。

  3. 一个进程中的多个线程共享相同的 内存单元 / 内存地址空间,可以访问相同的变量和对象,而且它们从同一堆中分配对象并进行通信、数据交换和同步操作。

  4. 由于线程间的通信是在同一地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快。

  5. 线程的启动、中断、消亡,消耗的资源非常少。

1.3 线程和进程的区别

  1. 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销。

  2. 线程可以看成是轻量级的进程,属于同一进程的线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。

  3. 线程和进程最根本的区别在于:进程是资源分配的单位,线程是调度和执行的单位。

  4. 多进程: 在操作系统中能同时运行多个任务(程序)。

  5. 多线程: 在同一应用程序中有多个顺序流同时执行。

  6. 线程是进程的一部分,所以线程有的时候被称为轻量级进程。

  7. 一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的。

  8. 系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。那就是说,除了CPU之外(线程在运行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。

1.4 进程、线程的选择取决

  1. 需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的。

  2. 线程的切换速度快,所以需要大量计算,切换频繁的用线程,还有耗时的操作使用线程可提高应用程序的响应

  3. 因为对CPU系统的效率使用上线程更占优,所以可能要发展到多机分布的用进程多核分布用线程

  4. 并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求;

  5. 需要更稳定安全时,适合选择进程;需要速度时,选择线程更好


二、实现方式

2.1 Thread

  • Thread多线程不保证立即运行。
  • 将此线程放到就绪队列中,如果有处理机可用,则执行run方法。
public class TestThread extends Thread {//自定义类继承Thread类
    //run()方法里是线程体
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(this.getName() + ":" + i);//getName()方法是返回线程名称
        }
    }

    public static void main(String[] args) {
        TestThread thread1 = new TestThread();//创建线程对象
        thread1.start();//启动线程
        TestThread thread2 = new TestThread();
        thread2.start();
    }
}

2.2 Runnable

  • 避免单继承的局限性,优先使用Runnable接口
  • 方便共享资源
public class TestRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);//getName()方法是返回线程名称
        }
    }

    public static void main(String[] args) {
        new Thread(new TestRunnable()).start();
        new Thread(new TestRunnable()).start();
    }
}

2.3 Callable

  • 步骤
    1. 创建目标对象: 继承Callable<E>的类object
    2. 创建执行服务: ExecutorService ser=Executors.newFixedThreadPool(num);
    3. 提交执行: Future<E> result = ser.submit(object); Future表示将要执行完任务的结果
    4. 获取结果: E r = result.get();阻塞形式等待Future中的任务异步处理结果
    5. 关闭服务: ser.shutdownNow();
import java.util.concurrent.*;

public class TestCallable implements Callable<Boolean> {
    private String url;     // 远程路径

    public TestCallable(String url) {
        this.url = url;
    }
    @Override
    public Boolean call() throws Exception {
        if(this.url.equals(""))
            return false;
        else{
            System.out.println(this.url);
            return true;
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestCallable t1 = new TestCallable("hahah");
        TestCallable t2 = new TestCallable("");
        TestCallable t3 = new TestCallable("afuo");
        
        ExecutorService ser= Executors.newFixedThreadPool(3);
        
        Future<Boolean> result1 = ser.submit(t1);
        Future<Boolean> result2 = ser.submit(t2);
        Future<Boolean> result3 = ser.submit(t3);
        
        Boolean r1 = result1.get();
        Boolean r2 = result2.get();
        Boolean r3 = result3.get();
        
        ser.shutdownNow();
    }
}

2.4 三者区别

  • 实现Runnable接口可以避免lava单继承特性而带来的局限
    增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的
    适合多个相同程序代码的线程区处理同一资源的情况

  • 继承Thread类和实现Runnable方法启动线程都是使用start方法,
    然后JVM虚拟机将此线程放到就绪队列中
    如果有处理机可用,则执行run方法。

  • 实现Callable接口要实现call方法,并且线程执行完毕后会有返回值
    其他的两种都是重写run方法,没有返回值。


三、线程状态

在这里插入图片描述

3.1 暂停线程执行

  • sleep()

    • sleep存在异常InterruptedException;
    • sleep时间达到后线程进入就绪状态;
    • sleep可以模拟网络延时倒计时等。
    • 每一个对象都有一一个锁,sleep不会释放锁;
  • yield()

    • 礼让线程,让当前正在执行线程暂停
    • 不是阻塞线程,而是将线程从运行状态转入就绪状态
    • 让cpu调度器重新调度

3.2 wait()、sleep()区别

  • sleep()方法 (休眠)是线程类( Thread )的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态)。

  • wait()Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池( lock pool ),如果线程重新获得对象的锁就可以进入就绪状态。

3.3 线程的联合join()

  • 让线程B和线程A联合。线程A就必须等待线程B执行完毕后,才能继续执行。

3.4 守护线程

  • 为用户线程服务
  • JVM停止不用等待守护线程执行完毕
  • 默认情况:JVM等待用户线程执行完毕才会停止

3.5 多线程信息交互

  • Object中的方法
    1. wait()
    2. notify()
    3. lock()

四、线程安全

4.1 定义

  • 某个类的行为与其规范一致
  • 不管多个线程怎样的执行顺序、优先级,或是wait, sleep, join等控制方式,如果一个类在多线程访问下运转一切正常,并且访问类不需要进行额外的同步处理或者协调,则线程安全

4.2 如何保证线程安全

  • 对变量使用volatile
  • 对程序段进行加锁(synchronizedlock

4.3 注意

  • 非线程安全的集合在多线程环境下可以使用,但并不能作为多个线程共享的属性,可以作为某个线程独享的属性。

  • 例如Vector是线程安全的, ArrayList不是线程安全的。如果每一个线程中new一个ArrayList ,而这个 ArrayList 只是在这一个线程中使用,肯定没问题。

4.4 多线程共用一个数据变量注意事项

  • ThreadLocal 是 JDK 引入的一种机制,它用于解决线程间共享变量,使用 ThreadLocal 声明的变量,即使在线程中属于全局变量,针对每个线程来讲,这个变量也是独立的。

  • volatile变量每次被线程访问时,都强迫线程从主内存中重读该变量的最新值,而当该变量发生修改变化时,也会强迫线程将最新的值刷新回主内存中。这样一来,不同的线程都能及时的看到该变量的最新值。


五、synchronized

5.1 概念

  • 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。

  • 线程同步其实就是一种等待机制:多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。

  • 解决的问题问题:访问冲突的问题

5.2 synchronized 方法

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

5.3 synchronized块

  • synchronized 方法的缺陷

    • 若将一个大的方法声明为synchronized 将会大大影响效率。
  • 更好的解决办法: synchronized 块。

    • 块可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率

    • synchronized 块:通过 synchronized关键字来声明synchronized 块,语法如下:

      synchronized(syncObject){ 
         允许访问控制的代码 
      }
      
      • 同步块: synchronized (obi) { }, ob j称之为同步监视器
      • obj可以是任何对象,但是推荐使用共享资源作为同步监视
      • 同步方法中无需指定同步监视器,因为同步方法的同步监视器是this即该对象本身,或class即类的模子
  • 同步监视器的执行过程

    • 第一个线程访问, 锁定同步监视器,执行其中代码
    • 第二个线程访问,发现同步监视器被锁定,无法访问
    • 第一个线程访问完毕,解锁同步监视器
    • 第二个线程访问,发现同步监视器未锁,锁定并访问
  • 示例

public class TestSynchronized2 {
    public static void main(String[] args) {
        // 1个资源
        SynTest synTest = new SynTest();
        // 多个代理
        new Thread(synTest, "张三").start();
        new Thread(synTest, "李四").start();
        new Thread(synTest, "王五").start();
    }
}

class SynTest implements Runnable{

    // 票数
    private int ticketsNums = 10;
    private boolean flag = true; //循环条件

    @Override
    public void run() {
        while(flag){
            try {
                Thread.sleep(100);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            // test();
            // test2();
            // test3();
            test5();
        }
    }

    // 线程安全,同步方法
    private synchronized void test() {
        if(ticketsNums <=0 ){
            flag = false;
            return ;
        }
        try {
            Thread.sleep(200);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "---->"+ ticketsNums -- );
    }

    // 线程安全,同步块
    // 对象的属性在变
    private void test2() {
        synchronized(this){
            if(ticketsNums <=0 ){
                flag = false;
                return ;
            }
            try {
                Thread.sleep(200);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "---->"+ ticketsNums -- );
        }
    }

    // 线程不安全,同步块
    // ticketsNums对象在变
    private void test3() {
        synchronized((Integer)ticketsNums){
            if(ticketsNums <=0 ){
                flag = false;
                return ;
            }
            try {
                Thread.sleep(200);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "---->"+ ticketsNums -- );
        }
    }
    
	// 线程安全
    // 范围尽可能合理
    // 双重检测:缩小锁定范围
    private void test5() {
        if(ticketsNums <=0 ){ // 考虑无票情况
            flag = false;
            return ;
        }
        synchronized(this){
            if(ticketsNums <=0 ){ // 考虑最后一张情况
                flag = false;
                return ;
            }
            try {
                Thread.sleep(200);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "---->"+ ticketsNums -- );
        }
    }
}

5.4 同步块、同步方法的区别

  • 参考链接

  • synchronized用在代码块上,锁的是调用该方法的对象( this ),也可以选择锁住任何一个对象

    • 用在类实例的对象上,锁住的是当前的类的实例
    • 用在类对象上,锁住的是该类的类对象。
  • synchronized用在方法上锁的是调用该方法的对象,

    • 使用在static方法上,synchronized锁住的是类对象
    • 使用在实例方法上,synchronized锁住的是实例对象
  • synchronized用在代码块可以减小锁的粒度,从而提高并发性能

  • 无论用在代码块上还是用在方法上,都是获取对象的锁;
    每一个对象只有一个锁与之相关联;
    实现同步需要很大的系统开销作为代价,甚至可能造成死锁,所以尽量避免无谓的同步控制。

5.5 Synchronized与Static Synchronized的区别

  • 参考链接

  • static只能加在方法上

  • synchronized对类的当前实例进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块,同一个类的两个不同实例就没有这种约束了。

  • 那么static synchronized恰好就是要控制类的所有实例的访问了,
    static修饰的锁是类锁,锁住的是整个Class,
    static synchronized是限制线程同时访问jvm中该类的所有实例同时访问对应的代码快。

5.6 Synchronized底层实现、源码

5.6.1 Synchronized在代码块

public class Test implements Runnable {
    @Override
    public void run() {
        // 加锁操作
        synchronized (this) {
            System.out.println("hello");
        }
    }
    public static void main(String[] args) {
        new Thread(new Test()).start();
    }
}

class文件反编译
在这里插入图片描述

  • 根据《Java虚拟机规范》的要求

    1、 在执行monitorenter指令的时候,首先要去尝试获取对象的锁(获取对象锁的过程,其实是获取monitor对象的所有权的过程)。

    2、如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一。

    3、 而在执行monitorexit指令时会将锁计数器减一。一旦计数器的值为零,锁随即就被释放了。

    4、 如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。

  • 为什么会 有两个monitorexit 呢?

    主要是防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)。因此最后一个monitorexit是保证在异常情况下,锁也可以得到释放,避免死锁。

5.6.2 Synchronized在方法

public class Test implements Runnable {
    @Override
    public synchronized   void run() {
            System.out.println("hello again");
    }

    public static void main(String[] args) {
        Test test = new Test();
        Thread thread = new Thread(test);
        thread.start();
    }
}

在这里插入图片描述

  • ACC_SYNCHRONIZED标志。

    • 代表的是当线程执行到方法后会检查是否有这个标志,如果有的话就会隐式的去调用monitorenter和monitorexit两个命令来将方法锁住。
  • synchronized可重入的原理

    • 重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁
    • 底层原理维护一个计数器,当线程获取该锁时,计数器加一,再次获得该锁时继续加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。

六、lock

6.1 常用方法

  • void lock() ,获取锁,如果锁被占用,则等待
  • boolean tryLock(),尝试获取锁,成功为True,失败为False,不阻塞
  • void unlock()释放锁

6.2 Synchronized、lock区别和使用场景

  • 使用场景

    • synchronized(隐式锁)
      • 在方法上使用 Synchronized
        • 方法声明时使用,放在范围操作符之后,返回类型声明之前。即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候。
      • 在某个代码块使用 Synchronized
        • 括号中表示需要锁的对象。表示只能有一个线程进入某个代码段。
    • lock(显示锁)
      • 需要显示指定起始位置和终止位置。
      • 一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。
      • 且在加锁和解锁处需要通过lock()unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
  • 区别

    • 存在层面
      • Syncronized 是Java 中的一个关键字,存在于 JVM 层面,托管给JVM执行的
      • Lock 是 Java 中的一个接口
    • 锁的释放条件
      • Synchronized
      1. 获取锁的线程执行完同步代码后,自动释放;
      2. 线程发生异常时,JVM会让线程释放锁;
      • Lock 必须在 finally 关键字中释放锁,不然容易造成线程死锁
    • 锁的获取
      • 在 Syncronized 中,假设线程 A 获得锁,B 线程等待。如果 A 发生阻塞,那么 B 会一直等待。在 Lock 中,会分情况而定。
      • Lock 中有尝试获取锁的方法,如果尝试获取到锁,则不用一直等待
    • 锁的状态
      • Synchronized 无法判断锁的状态
      • Lock 则可以判断
    • 锁的类型
      • Synchronized 是可重入,不可中断,非公平锁
        • synchronized原始采用的是 CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。
      • Lock 锁则是可重入,可中断,可公平锁
        • Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是 CAS操作( Compare and Swap)
    • 锁的性能
      • Synchronized 适用于少量同步的情况下,性能开销比较大。
      • Lock 锁适用于大量同步阶段:
        • Lock 锁可以提高多个线程进行读的效率(使用 ReadWriteLock)
        • 在竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
        • ReetrantLock 提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。

6.3 Lock类型

6.3.1 可重入锁

  • 可重入锁ReentrantLock又名递归锁
    • 可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Ticket implements Runnable{
    private Lock lock = new ReentrantLock();
    private int ticketsNums = 20;
    boolean flag = true;
    @Override
    public void run() {
        while(flag){
            try {
                Thread.sleep(200);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            if(ticketsNums <=0 ){ // 考虑无票情况
                flag = false;
                return ;
            }

            lock.lock();
            try{

                if(ticketsNums <=0 ){ // 考虑临界情况
                    flag = false;
                    return ;
                }
                try {
                    Thread.sleep(200);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "---->"+ ticketsNums -- );
            }finally {
                lock.unlock();
            }

        }
    }
}
public class 可重入锁 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        ExecutorService es = Executors.newFixedThreadPool(3);
        for(int i = 0; i<3;i++){
            es.submit(ticket);
        }
        es.shutdown();
    }
}

6.3.2 读写锁、互斥锁

  • 互斥锁在Java中的具体实现就是ReentrantLock

  • 读写锁在Java中的具体实现就是ReadWriteLock

    • 读写锁ReentrantReadWriteLock:,继承了ReadWriteLock

      • 一种支持一写多读的同步锁,读写分离,可分别分配读锁、写锁。

      • 支持多次分配读锁,使多个读操作可以并发执行。写锁是独占的。

      • 所有读写锁的实现必须确保写操作对读操作的内存影响。换句话说,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容

      • 读写锁比互斥锁允许对于共享数据更大程度的并发。每次只能有一个写线程,但是同时可以有多个线程并发地读数据。适用于读多写少的并发情况

    • 互斥规则:

      • 写~写:互斥,阻塞。
      • 读~写:互斥,读阻塞写、写阻塞读。
      • 读~读:不互斥、不阻塞。
      • 在读操作远远高于写操作的环境中,可在保障线程安全的情况下,提高运行效率。

测试示例

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class ReadWriteDemo{
    // 创建读写锁
    private ReentrantReadWriteLock rrl = new ReentrantReadWriteLock();
    // 获取读锁
    private ReentrantReadWriteLock.ReadLock readLock = rrl.readLock();
    // 获取写锁
    private ReentrantReadWriteLock.WriteLock writeLock = rrl.writeLock();

    // 互斥锁,读~读也互斥
    private ReentrantLock lock = new ReentrantLock();

    private String value;

    //写入
    public void setValue(String value) {
        // 用写锁上锁
        writeLock.lock();
        try {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.value = value;
            System.out.println("写入:"+this.value);
        } finally {
            writeLock.unlock();
        }
    }
    //读取
    public String getValue() {
        // 用读锁上锁
        readLock.lock();
        try {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("读取:"+this.value);
            return this.value;
        } finally {
            readLock.unlock();
        }
    }
}

public class 读写锁 {
    public static void main(String[] args) {
        ReadWriteDemo readWriteDemo = new ReadWriteDemo();

        Runnable read = readWriteDemo::getValue;
        Runnable write = () -> readWriteDemo.setValue("张三"+new Random().nextInt(100));

        ExecutorService es = Executors.newFixedThreadPool(20);
        long start = System.currentTimeMillis();

        // 分配2个写的任务
        for(int i = 0; i < 3; i++){
            es.submit(write);
        }
        // 分配10个读的任务
        for(int i = 0; i < 10; i++){
            es.submit(read);
        }
        es.shutdown();//关闭
        while (!es.isTerminated());//空转

        long end = System.currentTimeMillis();
        System.out.println("用时:"+ (end-start));
    }
}

6.3.3 公平锁、非公平锁

6.3.4 独享锁、共享锁

6.3.5 乐观锁、悲观锁

  • 乐观锁实现机制:CAS

6.3.6 分段锁

6.3.7偏向锁、轻量级锁、重量级锁

6.3.8 自旋锁

6.4 AQS

  • 参考资料

  • AQS的核心思想

    • 如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
    • CLH队列(Craig,Landin,and Hagersten)是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例仅存在节点之间的关联关系
    • 将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
    • 注意:AQS是自旋锁 :在 等待唤醒 的时候,经常会使用自旋while(!cas())的方式,不停地尝试获取锁,直到被其他线程获取成功

七、volatile关键字

7.1 作用

  • 保证内存的可见性,只保证数据的同步,即保证变量修改的实时可见性。
    • 内存可见性
      • volatile保证可见性的原理在每次访问变量时都会进行次刷新 ,因此每次访问都是主内存中最新的版本。所以volatile关键字的作用之一就是保证变量修改的实时可见性。
  • 防止指令重排
  • 不能保证原子性

7.2 使用条件

  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值
  • 该变量没有包含在具有其他变量的不变式中

7.3 使用建议

  • 在两个或者更多的线程需要访问的成员变量上使用volatile
    要访问的变量已在synchronized代码块中,或者为常量时,没必要使用volatile
  • 由于使用volatile屏蔽掉了JVM中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。

7.4 volatile、Synchronized的区别

  • volatile不会进行加锁操作
    volatile变量是一种稍弱的同步机制在访问volatile变量 时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字 更轻量级的同步机制。
  • volatile 变量作用 类似于同步变量读写操作
    从内存可见性的角度看, 写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。
  • volatile 不如synchronized安全
    在代码中如果过度依赖volatile变量来控制状态的可见性,通常会比使用锁的代码更脆弱,也更难以理解。仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它。一般来说,用同步机制会更安全些。
  • volatile 无法同时保证内存可见性和原子性
    加锁机制(即Synchronized同步机制)既可以确保可见性又可以确保原子性
    而volatile变量只能确保可见性,原因 是声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用
    也就是说如下的表达式都不是原子操作: “count++” 、“count = count+1”。

八、线程池

8.1 常用的线程池接口和类(所在包java. util. concurrent):

  • Executor: 线程池的顶级接口。
  • ExecutorService: 线程池接口,可通过submit (Runnable task) 提交任务代码。
  • Executors工厂 类:通过此类可以获得一个线程池。
  • 通过 newFixedThreadPool (int nThreads) 获取固定数量的线程池。
    参数:指定线程池中线程的数量。
  • 通过 newCachedThreadPool() 获得动态数量的线程池,如不够则创建新的,没有上限

8.2 线程池创建

  • 使用场景

    • 单个任务处理时间比较短
    • 需要处理的任务数量很大
  • 作用

    • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
    • 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
    • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • 线程池类型

    • 1、创建固定线程个数线程池
    • 2、创建缓存线程池,由任务的多少决定
    • 3、创建单线程池
    • 4、创建调度线程池调度:周期、定时执行
    public class 线程池 {
        public static void main(String[] args) {
            // 0、创建任务
            // 抢票
            Runnable runnable = new Runnable(){
                private int ticketsNums = 20;
                boolean flag = true;
                @Override
                public void run() {
                    while(flag){
                        try {
                            Thread.sleep(100);
                        }catch (InterruptedException e){
                            e.printStackTrace();
                        }
                        if(ticketsNums <=0 ){ // 考虑无票情况
                            flag = false;
                            return ;
                        }
                        synchronized (this){
                            if(ticketsNums <=0 ){ // 考虑无票情况
                                flag = false;
                                return ;
                            }
                            try {
                                Thread.sleep(100);
                            }catch (InterruptedException e){
                                e.printStackTrace();
                            }
                            System.out.println(Thread.currentThread().getName() + "---->"+ ticketsNums -- );
                        }
                    }
                }
            };
    
            // 1.1、固定线程个数的线程池
            ExecutorService ser1= Executors.newFixedThreadPool(3);
            // 1.2、缓存线程池,线程个数由任务个数决定
            ExecutorService ser2= Executors.newCachedThreadPool();
            // 1.3、单线程线程池
            Executors.newSingleThreadExecutor();
            // 1.4、调度线程池,调度:周期、定时执行
            // Executors.newScheduledThreadPool(corePoolSize);
    
            // 2、提交任务
            for(int i = 0; i < 3; i++){
                ser1.submit(runnable);
            }
            // 3、关闭线程池
            // 启动一次顺序关闭,执行完毕之前提交的任务,但不接受新任务,然后关闭线程池
            ser1.shutdown();
            // 停止正在执行的任务,暂停正在等待的任务,并返回等待执行任务的列表
            // ser.shutdownNow();
        }
    }
    

8.3 ThreadPoolExecutor

参考资料

  1. 深入理解Java线程池原理
  2. 彻底理解Java线程池原理篇
  • 设计线程池要注意的地方
    • 基本组成部分

      • 线程管理器(ThreadPool) :用于创建并管理线程池,包括创建线程,销毁线程池,添加新任务;
      • 工作线程(PoolWorker) :线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
      • 任务接口(Task) :每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
      • 任务队列(TaskQueue) :用于存放没有处理的任务。提供一种缓冲机制;
    • 包含的方法

      • 构造函数,创建线程池
      • 获取默认线程个数的线程池
      • 执行任务(将任务加入任务队列,由线程池管理器决定执行时间)
      • 批量执行任务
      • 销毁线程池(保证所有任务都完成后才销毁所有线程,否则等待任务完成)
      • 返回工作线程个数
      • 返回已完成任务个数(这里的已完成指的是出了任务队列的任务,任务可能并未实际执行完成)
      • 增加线程池中线程个数(前提:保证线程池中所有线程正在执行,且要执行线程的个数大于某一值)
      • 缩减线程池中线程个数(前提:保证线程池中很大一部分线程初一空闲状态,且,空闲状态的线程小于某一值)

8.4 ThreadLocal

  • 参考资料

  • 用途

    • 保存线程上下文信息,在任意需要的地方可以获取!!!
    • 线程安全的,避免某些情况需要考虑线程安全必须同步带来的性能损失!!!
    • 线程间数据隔离
  • 局限性

    • 无法解决共享对象的更新问题

九、死锁及其解决方法

参考链接

  1. 死锁原理、检测以及解决方法

9.1 死锁

  • 产生的四个必要条件
    • 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
    • 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
    • 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
    • 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

9.2 解决方法

  • 统一加锁顺序
  • 请求锁时限&失败返回
    • 尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。
  • 等待中断
  • 无锁编程
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值