【java多线程】1 Thread、线程池、同步总结


JUC笔记:https://blog.csdn.net/hancoder/article/details/105740321

笔记来源:小量传智播客+个人填充

Thread类

Thread常用方法

//构造方法
Thread();
Thread(String threadname);//创建线程并指定线程实例名
Thread(Runnable r);//指定创建线程的目标对象,它实现了Runnable接口中的run方法
Thread(Runnable r, String name);

Thread(FutureTask<> ft);//FutureTask<>(Callable<>);

常用方法:

void start(); 启动线程,并执行对象的run()方法
run(): 线程在被调度时执行的操作
String getName(): 返回线程的名称
void setName(String name):设置该线程名称
static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类  

static void yield(): 线程让步,让当前正在执行的线程暂停,但不阻塞,将线程从运行状态转为就绪状态。让cpu重新调度,礼让不一定成功,看CPU心情
//暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
//若队列中没有同优先级的线程,忽略此方法
  
    
join() : 当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止
//低优先级的线程也可以获得执行

static void sleep(long millis)//令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
sleep存在异常InteruptedException;
sleep时间达到后线程进入就绪状态;
sleep可以模拟网延时,倒计时等;
每一个对象都有一个锁,sleep不会释放锁;

stop(): 强制线程生命期结束,不推荐使用
destroy()方法(已废弃)
boolean isAlive(): 返回boolean,判断线程是否还活着

创建多线程的方式:

1.继承Thread
2.实现Runnable
3.实现Callable
4.使用线程池

① 继承Thread类

  1. 定义子类继承Thread类。
  2. 子类中重写Thread类中的run方法。
  3. 创建Thread子类对象,即创建了线程对象。
  4. 调用线程对象start方法:启动线程,调用run方法

注意点:
\1. 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
\2. run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
\3. 想要启动多线程,必须调用start方法。
\4. 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常“IllegalThreadStateException”

②实现Runnable接口

  1. 定义子类,实现Runnable接口。
  2. 子类中重写Runnable接口中的run方法。
  3. 通过Thread类含参构造器Thread(Runnable target)创建线程对象。
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
  5. 调用Thread类的start方法,开启线程, 系统自动调用Runnable子类接口的run方法。

区别
 继承Thread:线程代码存放Thread子类run方法中。
 实现Runnable:线程代码存在接口的子类的run方法。
 避免了单继承的局限性
 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。

package com.atguigu.java2;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

//方式1:继承Thrad
class MyThread01 extends Thread {
    @Override
    public void run() {//无返回值
        System.out.println("-----MyThread01");
    }
}

//方式2:实现Runnable
class MyThread02 implements Runnable {//还可以实现、继承别的类
    public void run() {//无返回值
        System.out.println("-----MyThread02");
    }
}

//方式3:实现Callable<>
class MyThread03 implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {//有返回值//不是run,而是call
        System.out.println("-----MyThread03");
        return 200;
    }
}

public class ThreadNew {
    public static void main(String[] args) {
        //方式1 实现
        new MyThread01().start();
        //方式2 继承Runnable
        new Thread(new MyThread02()).start();//传入Runnable对象
        //方式3 Callable
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyThread03());//传入Callable对象
        new Thread(futureTask).start();
        try {
            Integer value = futureTask.get();//得到返回值
            System.out.println(value);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

线程的优先级

线程的优先级等级
MAX_PRIORITY: 10
MIN _PRIORITY: 1
NORM_PRIORITY: 5(默认)

getPriority() : 返回线程优先值
setPriority(int) : 改变线程的优先级
    
线程创建时继承父线程的优先级
低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用  

线程的分类

java中的线程分为两类:一种是守护线程,一种是用户线程。
 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
 守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
 java垃圾回收就是一个典型的守护线程。
 若JVM中都是守护线程,当前JVM将退出。
 形象理解: 兔死狗烹,鸟尽弓藏

守护线程
    线程分为用户线程和守护线程
    虚拟机必须确保用户线程执行完毕
    虚拟机不必等待守护线程执行完毕
    如:后台记录操作日志,监控内存,垃圾回收等待
    
    list不安全的原因是可能同一时间操作了同一个索引
    for(){
        new Thread(()->{list.add();}).start();
    }

线程的生命周期

JDK中用Thread.State类(线程的状态)定义了线程的几种状态
要想实现多线程, 必须在主线程中创建新的线程对象。 java语言使用Thread类及其子类的对象来表示线程, 在它的一个完整的生命周期中通常要经历如下的五种状态:

  • 新建NEW: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
  • 就绪: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
  • 运行: 当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
  • 阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
  • 死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
    NEW尚未启动
    RUNNABLE执行中
    BLOCKED
    WAITING正在等待另一个线程执行动作
    TIME_WAITING正在等待另一个线程执行动作达到指定等待时间
    TERMINATED结束

图11-4 线程生命周期图.png

线程的同步

  • 同步方法
  • 同步块
//1. 同步代码块:
synchronized (同步监视器(锁)对象){//任何一个类的对象,都可以充当锁
    ...// 需要被同步的代码;
}

//2.同步方法: synchronized还可以放在方法声明中,表示整个方法为同步方法。
//继承的需要static,实现的不需要,因为传入的都是一个Runable对象,this指针指向同一个,此时同步监视器对象是当前类.class对象
//继承的方式不一定非得加static,比如多个用户共用一个银行账户。银行账户用共用一个就无需static,因为只有一个账户
public synchronized void show (String name){
    ...
}

同步机制中的锁
 任意对象都可以作为同步锁。 所有对象都自动含有单一的锁(监视器) 。
 同步方法的锁:静态方法(锁class对象)、非静态方法(锁this对象)
 同步代码块:自己指定, 很多时候也是指定为this或类名.class
 注意:
 必须确保使用同一个资源的多个线程共用一把锁, 这个非常重要, 否则就无法保证共享资源的安全
 一个线程类中的所有静态方法共用同一把锁(类名.class) , 所有非静态方法共用同一把锁(this) , 同步代码块(指定需谨慎)

释放锁的操作
 当前线程的同步方法、同步代码块执行结束。
 当前线程在同步代码块、同步方法中遇到break、 return终止了该代码块、该方法的继续执行。
 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception, 导致异常结束。
 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁

不会释放锁的操作
线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
应尽量避免使用suspend()和resume()来控制线程

//单例模式之懒汉式(线程安全)
class Singleton {
    private static Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance==null){//把if写到外面效率高
            synchronized(Singleton.class){
                if(instance == null){
                    instance=new Singleton();
                }
            }
        }
        return instance;
    }
}
public class SingletonTest{
    public static void main(String[] args){
        Singleton s1=Singleton.getInstance();
        Singleton s2=Singleton.getInstance();
        System.out.println(s1==s2);
    }
}

联合join()

​ 线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行。如下面示例中,“爸爸线程”要抽烟,于是联合了“儿子线程”去买烟,必须等待“儿子线程”买烟完毕,“爸爸线程”才能继续抽烟。

public class TestThreadState {
    public static void main(String[] args) {
        System.out.println("爸爸和儿子买烟故事");
        Thread father = new Thread(new FatherThread());
        father.start();
    }
}
 
class FatherThread implements Runnable {
    public void run() {
        System.out.println("爸爸想抽烟,发现烟抽完了");
        System.out.println("爸爸让儿子去买包红塔山");
        Thread son = new Thread(new SonThread());//还没start
        son.start();
        System.out.println("爸爸等儿子买烟回来");
        try {
            son.join();//重点
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("爸爸出门去找儿子跑哪去了");
            // 结束JVM。如果是0则表示正常结束;如果是非0则表示非正常结束
            System.exit(1);
        }
        System.out.println("爸爸高兴的接过烟开始抽,并把零钱给了儿子");
    }
}
 
class SonThread implements Runnable {
    public void run() {
        System.out.println("儿子出门去买烟");
        System.out.println("儿子买烟需要10分钟");
        try {
            for (int i = 1; i <= 10; i++) {
                System.out.println("第" + i + "分钟");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("儿子买烟回来了");
    }
}

图11-8示例11-6运行效果图.png

③ Lock(锁)

 从JDK 5.0开始, java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
 java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
 ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 在实现线程安全的控制中,比较常用的是ReentrantLock, 可以显式加锁、释放锁

class A{
    private final ReentrantLock lock = new ReenTrantLock();//可以传入参数fair:true
    public void m(){
        lock.lock();//加锁
        // Condition condition2 = lock.newCondition() 
        try{
            //保证线程安全的代码;
            condition2.await();
        }
        finally{
            // condition1.signal();
            // conditin1.signalAll();
            lock.unlock();//解锁
        }
    }
}

synchronized 与 Lock 的对比
\1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁), synchronized是隐式锁,出了作用域自动释放
\2. Lock只有代码块锁, synchronized有代码块锁和方法锁
\3. 使用Lock锁, JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:Lock >>> 同步代码块(已经进入了方法体,分配了相应资源) >>> 同步方法(在方法体之外)

线程的通信

线程通信(等待唤醒机制):多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。

多个线程共同执行意见任务,但是都需要一个相同的资源,但资源有限,如何协调使用。这种协调可以是竞争也可以是协作。

比如:线程A店铺用来生成包子的,线程B顾客用来吃包子的,但是只能同时存在一个包子,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。

店铺生成完一个后,生产下一个时发现商品还在,所以就不能生产了,开始睡眠(wait)。顾客到了,①如果有包子,等顾客吃了后,通知(notify)店铺可以生产了。②如果没包子,就需要等待wait,等店铺生产完通知notify。

等待唤醒机制其实就是经典的“生产者与消费者”的问题。

wait() 与 notify() 和 notifyAll()

  • 对象.wait():令当前线程挂起并放弃CPU、 释放同步资源并等待, 使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后+CPU才能继续执行,继续执行指的是从断点处恢复执行。同时,恢复执行的锁对象必须与notify通知的对象一致
  • 对象.notify():唤醒正在排队等待相同同步资源的线程中优先级最高者结束等待
  • 对象.notifyAll ():唤醒正在排队等待资源的所有线程结束等待.
  • 这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常。
  • 因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明、
  • wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
  • wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。

代码演示:

包子资源类:

public class BaoZi {
     String  pier ;//皮
     String  xianer ;//馅
     boolean  flag = false ;//包子资源状态 是否存在
}

吃货线程类:

public class ChiHuo extends Thread{
    private BaoZi bz;

    public ChiHuo(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }
    @Override
    public void run() {
        while(true){
            synchronized (bz){
                if(bz.flag == false){//没包子
                    try {
                        bz.wait();//释放bz
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("吃货正在吃"+bz.pier+bz.xianer+"包子");
                bz.flag = false;
                bz.notify();//唤醒其他拿包子的对象
            }
        }
    }
}

包子铺线程类:

public class BaoZiPu extends Thread {

    private BaoZi bz;

    public BaoZiPu(String name,BaoZi bz){
        super(name);
        this.bz = bz;
    }

    @Override
    public void run() {
        int count = 0;
        //造包子
        while(true){
            //同步
            synchronized (bz){
                if(bz.flag == true){//包子资源  存在
                    try {
                        bz.wait();//释放bz
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 没有包子  造包子
                System.out.println("包子铺开始做包子");
                if(count%2 == 0){
                    // 冰皮  五仁
                    bz.pier = "冰皮";
                    bz.xianer = "五仁";
                }else{
                    // 薄皮  牛肉大葱
                    bz.pier = "薄皮";
                    bz.xianer = "牛肉大葱";
                }
                count++;

                bz.flag=true;
                System.out.println("包子造好了:"+bz.pier+bz.xianer);
                System.out.println("吃货来吃吧");
                //唤醒等待线程 (吃货)
                bz.notify();
            }
        }
    }
}

测试类:

public class Demo {
    public static void main(String[] args) {
        //等待唤醒案例
        BaoZi bz = new BaoZi();

        ChiHuo ch = new ChiHuo("吃货",bz);
        BaoZiPu bzp = new BaoZiPu("包子铺",bz);

        ch.start();
        bzp.start();
    }
}
/*
执行效果:

包子铺开始做包子
包子造好了:冰皮五仁
吃货来吃吧
吃货正在吃冰皮五仁包子
包子铺开始做包子
包子造好了:薄皮牛肉大葱
吃货来吃吧
吃货正在吃薄皮牛肉大葱包子
包子铺开始做包子
包子造好了:冰皮五仁
吃货来吃吧
吃货正在吃冰皮五仁包子
*/

可重入锁:

synchronized和Lock都是可重入锁。什么是可重入锁?

//synchronized的可重入锁
synchronized (this) {
    System.out.println("第1次获取锁,这个锁是:" + this);
    int index = 1;
    while (true) {
        synchronized (this) {
            System.out.println("第" + (++index) + "次获取锁,这个锁是:" + this);
        }
        if (index == 10) {
            break;
        }
        //无需手动释放
    }
}
//lock的可重入锁
try {
    lock.lock();
    System.out.println("第1次获取锁,这个锁是:" + lock);

    int index = 1;
    while (true) {
        try {
            lock.lock();
            System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock);

            try {
                Thread.sleep(new Random().nextInt(200));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (index == 10) {
                break;
            }
        } finally {
            lock.unlock();//需要手动释放
        }
    }

} finally {
    lock.unlock();//最后一个释放
    //只要退出函数的时候把全部锁都unlock就行,无所谓作用域。
}

ReentrantLock 和 synchronized 不一样,需要手动释放锁,所以使用 ReentrantLock的时候一定要手动释放锁,并且加锁次数和释放次数要一样。加锁和释放次数不一样导致死锁

在一个函数里随意重入,只要退出函数的时候把全部锁都unlock就行,无所谓作用域。

(1) synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看lock它的源代码,来看它是如何实现的。

(2) ReentrantLock 比 synchronized 增加了一些高级功能

相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)④ 性能已不是选择标准

  • ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • **ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。**ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器)线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。

如果你想使用上述功能,那么选择ReentrantLock是一个不错的选择。

④Callable接口

与使用Runnable相比, Callable功能更强大些

  • 相比run()方法,可以有返回值
  • 方法可以抛出异常
  • 支持泛型的返回值
  • 需要借助FutureTask类,比如获取返回结果

Future接口

  • 可以对具体Runnable、 Callable任务的执行结果进行取消、查询是否完成、获取结果等。
  • FutrueTask是Futrue接口的唯一的实现类
  • FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
Future

我们刚刚在学习java内置线程池使用时,没有考虑线程计算的结果,但开发中,我们有时需要利用线程进行一些计算,然后获取这些计算的结果,而java中的Future接口就是专门用于描述异步计算结果的,我们可以通过Future 对象获取线程计算的结果;

Future 的常用方法如下:
boolean cancel(boolean mayInterruptIfRunning)  // 试图取消对此任务的执行。返回取消成功还是取消失败的bool 
 V get() // 如有必要,等待计算完成,然后获取其结果。 
 V get(long timeout, TimeUnit unit)  //如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。 
 boolean isCancelled() //如果在任务正常完成前将其取消,则返回 true。 
 boolean isDone() //如果任务已完成,则返回 true。 
package com.itheima.demo04;

import java.util.concurrent.*;

/*练习异步计算结果*/
public class FutureDemo {
    public static void main(String[] args) throws Exception {
        //1:获取线程池对象
        ExecutorService es = Executors.newCachedThreadPool();
        //2:创建Callable类型的任务对象
        Future<Integer> f = es.submit(new MyCall(1, 1));
        //3:判断任务是否已经完成
        //test1(f);
        boolean b = f.cancel(true);
        //System.out.println("取消任务执行的结果:"+b);
        //Integer v = f.get(1, TimeUnit.SECONDS);//由于等待时间过短,任务来不及执行完成,会报异常
        //System.out.println("任务执行的结果是:"+v);
    }
    //正常测试流程
    private static void test1(Future<Integer> f) throws InterruptedException, ExecutionException {
        boolean done = f.isDone();
        System.out.println("第一次判断任务是否完成:"+done);
        boolean cancelled = f.isCancelled();
        System.out.println("第一次判断任务是否取消:"+cancelled);
        Integer v = f.get();//一直等待任务的执行,直到完成为止
        System.out.println("任务执行的结果是:"+v);
        boolean done2 = f.isDone();
        System.out.println("第二次判断任务是否完成:"+done2);
        boolean cancelled2 = f.isCancelled();
        System.out.println("第二次判断任务是否取消:"+cancelled2);
    }
}
class MyCall implements Callable<Integer>{
    private int a;
    private int b;
    //通过构造方法传递两个参数

    public MyCall(int a, int b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public Integer call() throws Exception {
        String name = Thread.currentThread().getName();
        System.out.println(name+"准备开始计算...");
        Thread.sleep(2000);
        System.out.println(name+"计算完成...");
        return a+b;
    }
}

⑤使用线程池

背景: 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。频繁创建线程和销毁线程需要时间

思路: 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。

java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

1 通过构造方法创建线程池

ThreadPoolExecutor构造方法

ThreadPoolExecutor 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。

创建好的线程池用ExecutorService接口接收。

// 用给定的初始参数创建一个新的ThreadPoolExecutor。
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

ThreadPoolExecutor构造函数重要参数分析

  • corePoolSize : 常驻核心线程数。如果等于0,则任务执行完成后,没有任何请求进入时销毁线程池的线程;如果大于0,即使本地任务执行完毕,核心线程也不会被销毁。这个值的设置非常关键,设置过大会浪费资源,设置的过小会导致线程频繁地创建或销毁。
  • maximumPoolSize : 线程池能够容纳同时执行的最大线程数,但是他仅在队列满的时候才能用备选。当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。从上方的示例代码中第一处来看,必须大于或等于1。如果待执行的线程数大于此值,需要借助第5个参数的帮助。缓存在队列中。如果maximumPoolSize 与corePoolSize 相等,即是固定大小线程池。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。当请求的线程数大于maximumPoolSize时,线程进入BlockingQueue 阻塞队列。后续示例代码中使用的LinkedBlockingQueue 是单向链表,使用锁来控制入队和出对的原子性,两个锁分别控制元素的添加和获取,是一个生产消费模型队列。
    • step1.调用ThreadPoolExecutor的execute提交线程,首先检查CorePool,如果CorePool内的线程小于CorePoolSize,新创建线程执行任务。
    • step2.如果当前CorePool内的线程大于等于CorePoolSize,那么将线程加入到BlockingQueue。
    • step3.如果不能加入BlockingQueue,在小于MaxPoolSize的情况下创建线程执行任务。
    • step4.如果线程数大于等于MaxPoolSize,那么执行拒绝策略。
  1. keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;keepAliveTime 表示线程池中的线程空闲时间,当空闲时间达到KeepAliveTime 值时,线程被销毁,直到剩下corePoolSize 个线程为止,避免浪费内存和句柄资源。在默认情况下,当线程池的线程大于corePoolSize 时,keepAliveTime 才会起作用。但是ThreadPoolExecutor的allowCoreThreadTimeOut 变量设置为ture时,核心线程超时后也会被回收。
  2. unit : keepAliveTime :TimeUnit 表示时间单位。keepAliveTime 的时间单位通常是TimeUnit.SECONDS。
  3. threadFactory :threadFactory 表示线程工厂。它用来生产一组相同任务的线程。线程池的命名是通过给这个factory增加组名前缀来实现的。在虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的。
  4. handler :饱和策略、执行拒绝策略的对象。当超过参数workQueue的任务缓存区上限的时候,就可以通过该策略处理请求,这是一种简单的限流保护。友好的拒绝策略可以使如下三种:

ThreadPoolExecutor 饱和策略定义:

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy(默认):抛出 RejectedExecutionException异常来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy 不处理新任务,直接丢弃掉
  • ThreadPoolExecutor.DiscardOldestPolicy 此策略将丢弃最早未处理的任务请求。

举个例子: Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,采用默认的拒绝策略。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。

线程池的使用
1. 创建线程池对象。
2. 创建Runnable接口子类对象。(task)
3. 提交Runnable接口子类对象。(take task)
4. 关闭线程池(一般不做)。

ExecutorService es = ThreadPoolExecutor(...);
线程池基本方法
//往线程池里提交任务
es.submit(Runnable);
Future<Integer> ftask = es.submit(Callable);//可以返回值
es.inVokeAny();
es.inVokeAll();

es.shutdown();//使当前未执行的线程继续执行,而不再添加新的任务Task,该方法不会阻塞。如果线程池内有任务,那么把这些任务执行完毕后,关闭线程池....
es.shutdownNow()//不再接受新的任务,并把任务队列中的任务直接移出掉,如果有正在执行的,尝试进行停止...
es.execute(Runnable);

《阿里巴巴java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

Executors 返回线程池对象的弊端如下:

  • FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。

2 预定义线程池

通过Executor 框架的工具类Executors来实现 我们可以创建三种类型的ThreadPoolExecutor:

  • FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
  • SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
  • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
  • 前3个本质是调用了ThreadPoolExecutor的构造方法
  • ScheduledThreadPool:有时间间隔要求的线程池
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
//构造函数:
ExecutorService es = =Executors.newCachedThreadPool();//:创建一个可根据需要创建新线程的线程池
Executors.newCachedThreadPool(ThreadFactory);//:线程池中的所有线程都使用ThreadFactory来创建,这样线程无需手动启动,自动执行
    
Executors.newFixedThreadPool(n);// 创建一个可重用固定线程数的线程池
Executors.newFixedThreadPool(int,ThreadFactory);

Executors.newSingleThreadExecutor();// :创建一个只有一个线程的线程池
Executors.newSingleThreadExecutor(ThreadFactory);
    
Executors.newScheduledThreadPool(n);//:创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
Cached、Fix、Single、ExecutorService
public class MyTest01{
    main(){
        test1();
        test2();
    }
    private static void test1(){
        //使用工程类获取线程池对象
        ExecutorService es = Executors.newCachedThreadPool();
        //ExecutorService es = Executors.newFixThreadPool(3);
        //ExecutorService es = Executors.newSingleThreadExecutor();
        //提交任务
        for(int i=1;i<=10;i++){
            es.submit(new MyRunnable1(i));
        }
        //3:关闭线程池,仅仅是不再接受新的任务,以前的任务还会继续执行
        es.shutdown();//已经提交完了
        //es.submit(new MyRunnable4(888));//不能再提交新的任务了
        
        //3:立刻关闭线程池,如果线程池中还有缓存的任务,没有执行,则取消执行,并返回这些任务
        //List<Runnable> list = es.shutdownNow();
        //System.out.println(list);
    }
    private static void test2(){
        //使用工程类获取线程池对象
        ExecutorService es = Executors.newCachedThreadPool(new ThreadFacory(){
            int n=1;
            @Override
            public Thread newThread(Runnable r ){return new Thread(r,"自定义线程名称"+n++);}//虽然叫新线程,但是用旧线程的任务也会通过这个函数,每个线程会重复利用
        });/*
        ExecutorService es = Executors.newFixThreadPool(3,new ThreadFacory(){
            int n=1;
            @Override
            public Thread newThread(Runnable r ){return new Thread(r,"自定义线程名称"+n++);}
        });
        *//*
        //ExecutorService es = Executors.newSingleThreadExecutor(new ThreadFacory(){
            int n=1;
            @Override
            public Thread newThread(Runnable r ){return new Thread(r,"自定义线程名称"+n++);}
        });
        */
        //提交任务
        for(int i=1;i<=10;i++){
            es.submit(new MyRunnable1(i));
        }
    }
}

/*
    任务类,包含一个任务编号,在任务中,打印出是哪一个线程正在执行任务
 */
class MyRunnable implements Runnable{
    private  int id;
    public MyRunnable(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        //获取线程的名称,打印一句话
        String name = Thread.currentThread().getName();
        System.out.println(name+"执行了任务..."+id);
    }
}
ScheduledExecutorService

延迟执行与每时间段间隔执行

ScheduledExecutorService是ExecutorService的子接口,具备了延迟运行或定期执行任务的能力,
常用获取方式如下:
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
   创建一个可重用固定线程数的线程池且允许延迟运行或定期执行任务;
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
          创建一个可重用固定线程数的线程池且线程池中的所有线程都使用ThreadFactory来创建,且允许延迟运行或定期执行任务; 
static ScheduledExecutorService newSingleThreadScheduledExecutor() 
          创建一个单线程执行程序,它允许在给定延迟后运行命令或者定期地执行。 
static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) 
           创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。 
ScheduledExecutorService常用方法如下:
// 提交任务
<V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) 
          延迟时间单位是unit,数量是delay的时间后执行callable。 //作用类似于submit
 ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) 
          延迟时间单位是unit,数量是delay的时间后执行command。  
 ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) 
         延迟时间单位是unit,数量是initialDelay的时间后,每间隔period时间重复执行一次command。 
 ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) 
          创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。 

任务时间30s,间隔时间1min,scheduleAtFixedRate下次执行时间是1min后,scheduleWithFixedDelay是执行完再间隔1min,总共90s

package com.itheima.demo03;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/*
    测试ScheduleExecutorService接口中延迟执行任务和重复执行任务的功能
 */
public class ScheduleExecutorServiceDemo01 {
    public static void main(String[] args) {
        //1:获取一个具备延迟执行任务的线程池对象
        ScheduledExecutorService es = Executors.newScheduledThreadPool(3);
        // ScheduledExecutorService es = Executors.newSingleThreadScheduledExecutor( new ThreadFactory() {

        //2:创建多个任务对象,提交任务,每个任务延迟2秒执行
        for (int i=1;i<=10;i++){
            es.schedule(new MyRunnable(i),2, TimeUnit.SECONDS);
            //原来是submit
             //2:创建多个任务对象,提交任务,每个任务延迟2秒执行
            // es.scheduleAtFixedRate(new MyRunnable2(1),1,2,TimeUnit.SECONDS);

            //2:创建多个任务对象,提交任务,每个任务延迟2秒执行
           //es.scheduleWithFixedDelay(new MyRunnable3(1),1,2,TimeUnit.SECONDS);
        }
        System.out.println("over");
    }
}
class MyRunnable implements Runnable{
    private int id;

    public MyRunnable(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(name+"执行了任务:"+id);
    }
}

自定义线程池:

使用示例:

  • 编程任务类MyTask,实现Runnable接口
  • 编程线程类MyWorker,用于执行任务,需要持有所有任务
  • 编写线程池类MyThreadPool,包含提交任务,执行任务的能力
  • 编写测试类MyTest,创建线程池对象,提交多个任务测试
public class MyTask implements Runnable{

    private int id;
    public MyTask(int id) { this.id = id; }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println("线程:"+name+" 即将执行任务:"+id);
        try {
            Thread.sleep(200);//每个任务200ms
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程:"+name+" 完成了任务:"+id);
    }

    @Override
    public String toString() {
        return "MyTask{" +"id=" + id + '}';
    }
}
public class MyWorker extends Thread {//用一个工作台让全部任务开始执行
    private String name;//保存线程的名字
    private List<Runnable> tasks;
    //利用构造方法,给成员变量赋值

    public MyWorker(String name, List<Runnable> tasks) {//list
        super(name);
        this.tasks = tasks;
    }

    @Override
    public void run() {
        //判断集合中是否有任务,只要有,就一直执行任务
        while (tasks.size()>0){
            Runnable r = tasks.remove(0);
            r.run();//出队执行
        }
    }
}
public class MyThreadPool {
    // 1:任务队列   集合  需要控制线程安全问题
    private List<Runnable> tasks = Collections.synchronizedList(new LinkedList<>());
    //2:当前线程数量
    private int num;
    //3:核心线程数量
    private int corePoolSize;
    //4:最大线程数量
    private int maxSize;
    //5:任务队列的长度
    private int workSize;

    public MyThreadPool(int corePoolSize, int maxSize, int workSize) {
        this.corePoolSize = corePoolSize;
        this.maxSize = maxSize;
        this.workSize = workSize;
    }

    //1:提交任务;
    public void submit(Runnable r){
        //判断当前集合中任务的数量,是否超出了最大任务数量
        if(tasks.size()>=workSize){
            System.out.println("任务:"+r+"被丢弃了...");
        }else {
            tasks.add(r);
            //执行任务
            execTask(r);
        }
    }
    //2:执行任务;
    private void execTask(Runnable r) {
        //判断当前线程池中的线程总数量,是否超出了核心数,
        if(num < corePoolSize){
            new MyWorker("核心线程:"+num,tasks).start();
            num++;
        }else if(num < maxSize){
            new MyWorker("非核心线程:"+num,tasks).start();
            num++;
        }else {
            System.out.println("任务:"+r+" 被缓存了...");
        }
    }
}
public class MyTest {

    public static void main(String[] args) {
        //1:创建线程池类对象;
        MyThreadPool pool = new MyThreadPool(4,8,40);
        //2: 提交多个任务
        for (int i = 0; i <100 ; i++) {
            //3:创建任务对象,并提交给线程池
            MyTask my = new MyTask(i);
            pool.submit(my);
        }
    }
}

综合案例

秒杀商品

案例介绍:
	假如某网上商城推出活动,新上架10部新手机免费送客户体验,要求所有参与活动的人员在规定的时间同时参与秒杀挣抢,假如有20人同时参与了该活动,请使用线程池模拟这个场景,保证前10人秒杀成功,后10人秒杀失败;
要求:
	1:使用线程池创建线程
	2:解决线程安全问题
思路提示:
	1:既然商品总数量是10个,那么我们可以在创建线程池的时候初始化线程数是10个及以下,设计线程池最大数量为10个;
	2:当某个线程执行完任务之后,可以让其他秒杀的人继续使用该线程参与秒杀;
	3:使用synchronized控制线程安全,防止出现错误数据;
代码步骤:
	1:编写任务类,主要是送出手机给秒杀成功的客户;
	2:编写主程序类,创建20个任务(模拟20个客户);
	3:创建线程池对象并接收20个任务,开始执行任务;
	(代码演示参考idea)

package com.itheima.demo05;
/*
    任务类:
        包含了商品数量,客户名称,送手机的行为;
 */
public class MyTask implements Runnable {
    //设计一个变量,用于表示商品的数量
    private static int id = 10;
    //表示客户名称的变量
    private String userName;

    public MyTask(String userName) {
        this.userName = userName;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(userName+"正在使用"+name+"参与秒杀任务...");
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (MyTask.class){
            if(id>0){
                System.out.println(userName+"使用"+name+"秒杀:"+id-- +"号商品成功啦!");
            }else {
                System.out.println(userName+"使用"+name+"秒杀失败啦!");
            }
        }
    }
}

package com.itheima.demo05;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/*
    主程序类,测试任务类
 */
public class MyTest {
    public static void main(String[] args) {
        //1:创建一个线程池对象
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,1, TimeUnit.MINUTES,new LinkedBlockingQueue<>(15));
        //2:循环创建任务对象
        for (int i = 1; i <=20 ; i++) {
            MyTask myTask = new MyTask("客户"+i);
            pool.submit(myTask);
        }
        //3:关闭线程池
        pool.shutdown();
    }
}

取款业务

案例介绍:
	设计一个程序,使用两个线程模拟在两个地点同时从一个账号中取钱,假如卡中一共有1000元,每个线程取800元,要求演示结果一个线程取款成功,剩余200元,另一个线程取款失败,余额不足;
要求:
	1:使用线程池创建线程
	2:解决线程安全问题

      思路提示:
               1:线程池可以利用Executors工厂类的静态方法,创建线程池对象;
               2:解决线程安全问题可以使用synchronized方法控制取钱的操作
               3:在取款前,先判断余额是否足够,且保证余额判断和取钱行为的原子性;

          (代码演示参考idea)


package com.itheima.demo06;

public class MyTask implements Runnable {
    //用户姓名
    private String userName;
    //取款金额
    private double money;
    //总金额
    private static double total = 1000;

    public MyTask(String userName, double money) {
        this.userName = userName;
        this.money = money;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(userName+"正在准备使用"+name+"取款:"+money+"元");
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (MyTask.class){
            if(total-money>0){
                System.out.println(userName+"使用"+name+"取款:"+money+"元成功,余额:"+(total-money));
                total-=money;
            }else {
                System.out.println(userName+"使用"+name+"取款:"+money+"元失败,余额:"+total);
            }
        }
    }
}
package com.itheima.demo06;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

public class MyTest {
    public static void main(String[] args) {
        //1:创建线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(2, new ThreadFactory() {
            int id = 1;

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "ATM" + id++);
            }
        });
        //2:创建两个任务并提交
        for (int i = 1; i <=2 ; i++) {
            MyTask myTask = new MyTask("客户" + i, 800);
            pool.submit(myTask);
        }
        //3:关闭线程池
        pool.shutdown();
    }
}

线程池的使用步骤可以归纳总结为五步 :

1:利用Executors工厂类的静态方法,创建线程池对象;

2:编写Runnable或Callable实现类的实例对象;

3:利用ExecutorService的submit方法或ScheduledExecutorService的schedule方 法提交并执行线程任务

4:如果有执行结果,则处理异步执行结果(Future)

5:调用shutdown()方法,关闭线程池

知识点补充

线程进程区别

区别进程线程
根本区别作为资源分配的单位调度和执行的单位
开销
所处环境
分配内存
包含关系
 \1. 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销。

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

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

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

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

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

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

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

volatile关键字

在 JDK1.2 之前,java的内存模型实现总是从主存(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 java 内存模型下,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成==数据的不一致==。

数据不一致

要解决这个问题,就需要把变量声明为volatile,这就指示 JVM,这个变量是不稳定的,每次使用这个变量都到主存中进行读取

说白了, volatile 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。

volatile关键字的可见性

并发编程的三个重要特性

  1. 原子性 : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。synchronized可以保证代码片段的原子性。
  2. 可见性 :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。
  3. 有序性 :代码在执行的过程中的先后顺序,java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化。

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

  • synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在:

  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在javaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些

  • 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞

  • volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。

  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。

CAS/ABA

什么是CAS:compare and swap

比如一个i=0(当前值E),我们想+1,那么结果是1(结果值V),当我们去写回的时候,检查当前新值N和印象中的值E是否一致。如果一致代表没有别的线程改,如果不一致代表被别的线程改过了。

如果被别的线程改过了,那么重新读心的当前值E,再继续进行修改操作。

如果一致的时候,也会存在ABA问题。

ABA:虽然一致,但是已经被其他若干个线程更改过又更改回了。(其他线程修改次数的值和原值相同)

如何感知ABA问题:把值加个版本号,读值的时候顺便把版本号也读走,比较的时候不仅比值,也比较版本值。

死锁

​ “死锁”指的是:

​ 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。

​ 因此, 某一个同步块需要同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。下面案例中,“化妆线程”需要同时拥有“镜子对象”、“口红对象”才能运行同步块。那么,实际运行时,“小丫的化妆线程”拥有了“镜子对象”,“大丫的化妆线程”拥有了“口红对象”,都在互相等待对方释放资源,才能化妆。这样,两个线程就形成了互相等待,无法继续运行的“死锁状态”。

class Lipstick {//口红类
 
}
class Mirror {//镜子类
 
}
class Makeup extends Thread {//化妆类继承了Thread类
    int flag;
    String girl;
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();
 
    @Override
    public void run() {
        // TODO Auto-generated method stub
        doMakeup();
    }
 
    void doMakeup() {
        if (flag == 0) {
            synchronized (lipstick) {//需要得到口红的“锁”;
                System.out.println(girl + "拿着口红!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
 
                synchronized (mirror) {//需要得到镜子的“锁”;
                    System.out.println(girl + "拿着镜子!");
                }
 
            }
        } else {
            synchronized (mirror) {
                System.out.println(girl + "拿着镜子!");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lipstick) {
                    System.out.println(girl + "拿着口红!");
                }
            }
        }
    }
 
}
 
public class TestDeadLock {
    public static void main(String[] args) {
        Makeup m1 = new Makeup();//大丫的化妆线程;
        m1.girl = "大丫";
        m1.flag = 0;
        Makeup m2 = new Makeup();//小丫的化妆线程;
        m2.girl = "小丫";
        m2.flag = 1;
        m1.start();
        m2.start();
    }
}

图11-14示例11-11运行效果图.png

死锁的解决方法

​ 死锁是由于“同步块需要同时持有多个对象锁造成”的,要解决这个问题,思路很简单,就是:同一个代码块,不要同时持有两个对象锁

class Lipstick {//口红类
 }
class Mirror {//镜子类
}
class Makeup extends Thread {//化妆类继承了Thread类
    int flag;
    String girl;
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();
 
    @Override
    public void run() {
        // TODO Auto-generated method stub
        doMakeup();
    }
 
    void doMakeup() {
        if (flag == 0) {
            synchronized (lipstick) {
                System.out.println(girl + "拿着口红!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
 
            }
            synchronized (mirror) {
                System.out.println(girl + "拿着镜子!");
            }
        } else {
            synchronized (mirror) {
                System.out.println(girl + "拿着镜子!");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (lipstick) {
                System.out.println(girl + "拿着口红!");
            }
        }
    }
}
 
public class TestDeadLock {
    public static void main(String[] args) {
        Makeup m1 = new Makeup();// 大丫的化妆线程;
        m1.girl = "大丫";
        m1.flag = 0;
        Makeup m2 = new Makeup();// 小丫的化妆线程;
        m2.girl = "小丫";
        m2.flag = 1;
        m1.start();
        m2.start();
    }
}

后续JUC:

https://blog.csdn.net/hancoder/article/details/105740321

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值