(1)线程学习笔记

目录

1-1.串行/并发/并行

串行:

 并行:

 并发:

 1-2.继承Thread类创建线程

Thread的start()方法

1-3 多线程带来的问题

1-4线程锁

1.synchronized加载方法上的理解

2.同步静态方法

 3.volatile的作用

1-5 原子操作类简介

1-6 CAS机制

1.CAS的缺点:

2.CAS问题的解决

1-6 原子变量类

什么是“原子变量类”?

1-7等待机制

1)等待通机制

2)线程wait/notify 暂停/唤醒

3)notifyAll 唤醒所有线程

4)生产者-消费者模式

1-8 ThreadLocal

1) ThreadLocal的使用

2) ThreadLocal指定初始值

1-9 Lock显示锁

1) ReentrantLock显示锁使用

3) lockInterruptibly()方法使用

4)tryLock() 带参方法

5)newCondition()方法

6)公平锁和非公平锁

7)Lock锁的一些常用方法

2-1读写锁(ReentranReadWriteLock)

2-1线程池

1)什么是线程池?

2)线程池的使用和方法

3)阻塞队列模式

4) ThreadFactory线程工厂


1-1.串行/并发/并行

在电脑运行时候,每个运行的程序是一个进程,在进程中有很多的线程,进程好比一个容器承载着这些线程,线程是用来执行任务的,一个进程可能有多个任务委派给多个线程.

一个进程可以开启多个线程,其中有一个主线程(main)来调用本进程中的其他线程。
我们看到的进程的切换,切换的也是不同进程的主线程
多线程可以让同一个进程同时并发处理多个任务,相当于扩展了进程的功能。

串行:

串行就是传统的执行方法,运行一个CPU处理一个线程的任务

 并行:

并行就是运行多个CUP处理多个线程的任务

 并发:

而并发却和上面两个有所区别,并发的本质其实是:一个CPU处理多个线程的任务,但是它是交替执行的,并不遵从程序的顺序,而是随机执行的(后面会讲到优先级)由操作系统调度,但是CPU其实还是一件一件的处理线程的,只不过是它来回切换的速度很快,人看不出来.

 1-2.继承Thread类创建线程

在Java中,如果要实现多线程那就要创建线程,创建线程就要使用到Thread类

Thread是实现了Runnble接口的一个实现类,而里面扩展了很多可以操作线程的方法

1.分析Thread的构造方法

Thread() 分配新的Thread对象
Thread(String name) 分配新的Thread对象


Thread(Runnable target) 注意:这里接收的是一个Runnble对象,也可以是实现了Runnble接口的实例对象,并且运行时也是使用传入对象的run方法

底层实现的是 target.run

 public Thread(Runnable target) {
        this(null, target, "Thread-" + nextThreadNum(), 0);
    }


Thread(Runnable target,String name) 分配新的Thread对象
 

但是像这样光创建一个线程并没有用,不仅要创建线程还要运行里面的任务所以我们要往线程里面添加任务

在Thread类中有一个run方法,我们可以将任务放到这个run方法里,所以我们要创建一个(MyThread)类来继承Thread类,然后重写run方法,将要执行的业务逻辑或者任务放到run方法中

创建好后,我们就有了一个线程类(MyThread)它里面有我们要执行的任务(就是run方法里面的代码)

现在要执行这个线程的话,首先回到main方法中,要实例化一个线程对象,所以要new MyThread对象

然后使用我们Thread的方法来执行这创建的线程.

Thread的start()方法

这个方法可以使一个线程开始运行run方法,实例化对象.start();

我们有了

        /*本类用于多线程编程实现方案一:继承Thread类来完成*/
        public class TestThread {
            public static void main(String[] args) {
                //4.创建线程对象进行测试
                /*4.new对应的是线程的新建状态
                 * 5.要想模拟多线程,至少得启动2个线程,如果只启动1个,是单线程程序*/
                MyThread t1 = new MyThread();
                MyThread t2 = new MyThread();
                MyThread t3 = new MyThread();
                MyThread t4 = new MyThread();
                /*6.这个run()如果直接这样调用,是没有多线程抢占执行的效果的
                 * 只是把这两句话看作普通方法的调用,谁先写,就先执行谁*/
                //t1.run();
                //t2.run();
                /*7.start()对应的状态就是就绪状态,会把刚刚新建好的线程加入到就绪队列之中
                 * 至于什么时候执行,就是多线程执行的效果,需要等待OS选中分配CPU
                 * 8.执行的时候start()底层会自动调用我们重写的run()种的业务
                 * 9.线程的执行具有随机性,也就是说t1-t4具体怎么执行
                 * 取决于CPU的调度时间片的分配,我们是决定不了的*/
                t1.start();//以多线程的方式启动线程1,将当前线程变为就绪状态
                t2.start();//以多线程的方式启动线程2,将当前线程变为就绪状态
                t3.start();//以多线程的方式启动线程3,将当前线程变为就绪状态
                t4.start();//以多线程的方式启动线程4,将当前线程变为就绪状态
            }
        }

//1.自定义一个多线程类,然后让这个类继承Thread
        class MyThread extends Thread{
            /*1.多线程编程实现的方案1:通过继承Thread类并重写run()来完成的 */
            //2.重写run(),run()里是我们自己的业务
            @Override
            public void run() {
                /*2.super.run()表示的是调用父类的业务,我们现在要用自己的业务,所以注释掉*/
                //super.run();
                //3.完成业务:打印10次当前正在执行的线程的名称
                for (int i = 0; i < 10; i++) {
                    /*3.getName()表示可以获取当前正在执行的线程名称
                     * 由于本类继承了Thread类,所以可以直接使用这个方法*/
                    System.out.println(i+"="+getName());
                }
            }
        }

当我们使用Thread.start()的时候这个线程就会进入启动(就绪状态),然后我们的CPU会将该线程运行起来(运行状态),线程在运行就是运行run重写的方法,当执行结束后线程就结束(死亡),这只是一个线程运行时候的样子,学习线程就是为了多线程运行,那多线程有哪些问题呢?

1-3 多线程带来的问题

线程可以多个运行,但是在多个线程执行同一个任务的时候就会发生问题,如果只是读取同一个任务的文件或者数据倒还好,但是如果发生读写的操作的话就会发生问题.

1.脏读:在读取同一个全局变量时(假定为int = 100),T1线程过来读取数据,但是同时T2也来访问这个数据并且将int改成了50,然后T1把这个50读取了过去并没有得到之前想要的数据,这就是脏读(反之亦然,用户想要读取T2修改过后的数据,结果T1先读到了修改前的数据)

2.重复读:还是同一个全局变量(int = 100),T1过来读取这个int值,然后执行- -操作,而T2也正好来读取这个int也执行- -操作,结果两个人读到的都是100,然后--变为99,这就会发生运行了两次却只得到了一次的结果.

3.覆盖:当两个线程使用同一个任务(Runnble接口)的时候,同时去对一个全局变量进行修改,T1将name修改为张三,而这时候T2线程也来修改将name改成李四,结果返回了一个李四,这是T2要的结果,但是T1返回的也是李四,这就出问题了.

从上面的问题看出,如果是无序的多线程编程,会发生很多意想不到的结果,因为多线程是无序的执行线程,你不知道什么时候这个方法会被什么线程访问到,所以就要去控制访问数据时候的线程.

1-4线程锁

多线程带来了线程不安全的问题,但是也有解决的方案,所以引入了锁的概念.

锁(lock),顾名思义,跟生活中的门锁很像,但是也可以说它是一把打开synchronized锁住东西的钥匙

线程只有拿到这个锁,才能运行synchronized里面的代码,而没拿到锁的线程则不能去运行,只能等到拿到锁的线程运行完毕,出现异常,或者停止时才会释放锁,被释放后可以再次被争抢,然后进入下一轮循环.

锁的内存语义:

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
锁释放和锁获取的内存语义:

线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息

Mutex Lock

监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

synchronized会把保护的代码块放入到监视区中保护,只有进入监视区的线程才能操作代码

synchronized(A){

xxxxxxxx(代码)

}

打个比方,人(线程)要去银行(监视区)取钱(要被保护的代码),钱在银行里面,人要进入银行才能取到钱

我们的代码也是这样,线程如果要去执行synchronized保护的代码,必须跳转到监视区才能执行代码

synchronized是线程锁的一种,它可以加在:
代码块上
synchronized(){代码块}
方法上(包括静态方法)
public synchronized void sy(){}
public synchronized static void sy(){}

类上面

public synchronized class Sy{}

1.synchronized加载方法上的理解

synchronized加在方法上的时候需要指定一个锁(监视器),而这个锁必须是他们共有的一把锁,如果分开有两把锁各用各的根本没有效果,所以需要使用同一把锁进行管理.

synchronized(锁),通常都是使用synchronized(this),因为需要使用synchronized的地方都是需要串行通过的,使用的是同一个对象,所以用this来当做锁控制,当然也可以使用一个常量进行控制.比如

Object static final objectlock = new Objcet(),也可以用其他的常量,这里只是举个例子.

2.同步静态方法

还有一种就是修饰静态方法的时候的锁对象,用的是该对象的class类型,看下面的代码:


public class User{


//这是一个静态方法,我把锁加载代码块上
public static void sy(){

//我如果想用synchronized来控制代码块同步,那就要给它加锁,而它的锁是这个类的.class对象

        synchronized(User.class){
                //被保护的代码块
                    System.Out.println("我是同步代码块");
}


}

//这也是静态方法,我把锁加在静态方法上
public synchronized static void ssy(){

//这里的syncharonized的锁对象其实也是User.class
sout("我是同步代码块");


}





}

这就是一种在类上面加锁,也可以理解为在这个类的class类型上加了锁.

锁可以加在这三个地方的话有什么不同呢?

当加在代码块上的时候,线程可以都进入这个方法执行部分代码然后等候,而如果加在方法上的话都要在方法外等候,类的话就更远了,要在类的外面等候锁.这就是大概的区别,也是涉及到锁的执行效率问题.

如果我在方法中有一个Thread.seelp(1000)休眠一秒钟,我将锁加在方法里面的代码块上

public void test(){
            
try{
//休眠1秒
Thread.sleep(1000);
}
catch(Exception e){
    e.getmessage();
}
        synchronized(this){

sout("我是同步代码块")
}
}

我们可以让线程先进入到方法中,线程开始执行休眠,休眠完后再拿锁运行代码,而如果将锁加在方法上的话每次多个线程来使用的话会被阻挡在外,而拿到锁的才能进去运行还要进行休眠,这样的话每次执行时间都比加在代码块上需要的时间长.

在同步代码块上运行,锁的范围越细粒,效率越高,但是还是要看业务需求来定锁的位置

 3.volatile的作用

正常情况下为了避免死锁,我们会用一些全局变量去控制线程的运行,当线程进入sleep状态又重新进入到运行状态的时候(将值从公共内存刷新到工作内存中),就可以读取到这个全局变量,进而将线程停止

public class tttt {

    public static void main(String[] args) throws InterruptedException {
        ooo o = new ooo();
        new Thread(new Runnable() {
            @Override
            public void run() {
                o.printChar();
            }
        }).start();

        Thread.sleep(1000);
        System.out.println("主程序停止");
        o.setLoop(false);
    }


    static class ooo {
        private boolean loop = true;

        public void setLoop(boolean loop) {
            this.loop = loop;
        }

        public void printChar() {
            System.out.println("线程开始了" + Thread.currentThread().getName());
            while (loop) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程结束了" + Thread.currentThread().getName());
        }
    }
}

运行结果:


线程开始了Thread-0
主程序停止
线程结束了Thread-0

Process finished with exit code 0

但是,如果线程没有进入休眠状态,而是一直运行的话(在自己的工作内存中读取这个值),这个方法就不管用了,因为线程在运行的时候无法读取到更改过后的全局变量(不可见性).所以会一直运行下去,或者出现死锁.

                               (去掉sleep后运行,发现Thread-0一直在运行,根本停不下来)

线程开始了Thread-0
主程序停止

我们在这个运行表示这里(全局变量)加上volatile,看一段下面的代码,引出volatile的用处:

static class ooo {
        //在全局标识这里加上volatile
        private volatile boolean loop = true;

        public void setLoop(boolean loop) {
            this.loop = loop;
        }

        public void printChar() {
            System.out.println("线程开始了" + Thread.currentThread().getName());
            while (loop) {

            }
            System.out.println("线程结束了" + Thread.currentThread().getName());
        }
    }
}

这样就可以将这个全局变量放入到公共内存(主内存)中,被其他工作线程看见和访问,而不是从工作内存中去读取.

volatile算作轻量级的锁,但是他没有原子性(要么操作一起成功 ,要么都不操做,理解为线程的同步性)

但是可以保证线程的可见性.

volatile和syncronized的区别就在这

volatile:轻量级,具有可见性,只能修饰变量,被修饰的变量可以被强制刷新到主内存,但是还是会被多线程重复操作(非原子性):

使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,但需要特别注意, volatile不能保证复合操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read(读) i; inc(自加); write(写) i,假如多个线程同时执行i++volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。

synchronized:重量级,具有可见性和原子性

synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行(保证了线程的原子性)。更重要的是,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性.

1-5 原子操作类简介

synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。

尽管JAVA 1.6为synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过过度,但是在最终转变为重量级锁之后,性能仍然比较低。所以面对这种情况,我们就可以使用java中的“原子操作类”。

所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。如AtomicBoolean,AtomicUInteger,AtomicLong等。它们分别用于Boolean,Integer,Long类型的原子性操作。

现在我们尝试使用AtomicInteger类:

使用AtomicInteger之后,最终的输出结果同样可以保证是200。并且在某些情况下,代码的性能会比synchronized更好。

而Atomic操作类的底层正是用到了“CAS机制”。

1-6 CAS机制

CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换。

CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

我们看一个例子:

1. 在内存地址V当中,存储着值为10的变量。

2. 此时线程1想把变量的值增加1.对线程1来说,旧的预期值A=10,要修改的新值B=11.

3. 在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

4. 线程1开始提交更新,首先进行A和地址V的实际值比较,发现A不等于V的实际值,提交失败。

5. 线程1 重新获取内存地址V的当前值,并重新计算想要修改的值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。

6. 这一次比较幸运,没有其他线程改变地址V的值。线程1进行比较,发现A和地址V的实际值是相等的。

7. 线程1进行交换,把地址V的值替换为B,也就是12.

从思想上来说,synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。

在java中除了上面提到的Atomic系列类,以及Lock系列类夺得底层实现,甚至在JAVA1.6以上版本,synchronized转变为重量级锁之前,也会采用CAS机制。

示例cas机制代码:

public class tttt {
    public static void main(String[] args) {


        Cas cas = new Cas();
               new Thread(new Runnable() {
            @Override
            public void run() {
                //设置一个拿到的值
                int values = 0;
                //这个是运算过后的期望值
                int needvalue;


                //如果value不是0继续循环--
                while (cas.getValue() != 0) {

                    //设置一个拿到的值
                    values = cas.getValue();

                    //这个是运算过后的期望值
                    needvalue = values - 1;
    

                    if (cas.CAS(needvalue, values)) {
                        System.out.println(Thread.currentThread().getName()+"修改成功,值:"+needvalue);
                    } else {
                        System.out.println(Thread.currentThread().getName()+"修改失败,值:"+values);
                    }

                }
            }
        }).start();


        c1.start();
    }
}


//模拟CAS机制
class Cas {
    //创建一个全局变量
    private static int value = 1000;
    //新的值
    private int newValue;
    //旧的值
    private int oldValue;

    public boolean CAS(int newValue, int oldValue) {

        //判断,如果修改value的值后,我之前拿到的oldValue和现在的value值不一致,则放弃修改value值
        //如果相等,则说明现在数据是正确的,则修改value
        if (oldValue == value) {
            value = newValue;
            return true;
        } else {
            return false;
        }

    }

当我new了两个线程去操作结果如下,可以看出来cas机制可以防止我们的线程操作单个变量出现非原子性问题.

                                                                                      

Thread-1修改成功,值:14
Thread-1修改成功,值:13
Thread-1修改成功,值:12
Thread-1修改成功,值:11
Thread-1修改成功,值:10
Thread-1修改成功,值:9
Thread-1修改成功,值:8
Thread-1修改成功,值:7
Thread-1修改成功,值:6
Thread-1修改成功,值:5
Thread-1修改成功,值:4
Thread-1修改成功,值:3
Thread-0修改成功,值:29
Thread-1修改成功,值:2
Thread-0修改成功,值:1
//线程是无序的,所以结果是无序的,但是结果是正确的,没有出现脏读或者覆盖修改哦!!!
Process finished with exit code 0

1.CAS的缺点:

1) CPU开销过大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。

2) 不能保证代码块的原子性

CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

3) ABA问题

这是CAS机制最大的问题所在。

我们现在来说什么是ABA问题。假设内存中有一个值为A的变量,存储在地址V中。

此时有三个线程想使用CAS的方式更新这个变量的值,每个线程的执行时间有略微偏差。线程1和线程2已经获取当前值,线程3还未获取当前值。

接下来,线程1先一步执行成功,把当前值成功从A更新为B;同时线程2因为某种原因被阻塞住,没有做更新操作;线程3在线程1更新之后,获取了当前值B。

在之后,线程2仍然处于阻塞状态,线程3继续执行,成功把当前值从B更新成了A。

最后,线程2终于恢复了运行状态,由于阻塞之前已经获得了“当前值A”,并且经过compare检测,内存地址V中的实际值也是A,所以成功把变量值A更新成了B。

简单的来说其实线程2这里要获取的A已经不是我们一开始期望的那个A了,或者说我们原来是希望是一开始的A进行更改而不是被其他值更改了后,又改回来的那个值进行改变.

2.CAS问题的解决

怎么解决呢?加个版本号就可以了。

真正要做到严谨的CAS机制,我们在compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。

我们仍然以刚才的例子来说明,假设地址V中存储着变量值A,当前版本号是01。线程1获取了当前值A和版本号01,想要更新为B,但是被阻塞了。

这时候,内存地址V中变量发生了多次改变,版本号提升为03,但是变量值仍然是A。

随后线程1恢复运行,进行compare操作。经过比较,线程1所获得的值和地址的实际值都是A,但是版本号不相等,所以这一次更新失败。

在Java中,AtomicStampedReference类就实现了用版本号作比较额CAS机制。

1-6 原子变量类

什么是“原子变量类”?

自JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作。原子变量的底层使用了处理器提供的原子指令,但是不同的CPU架构可能提供的原子指令不一样,也有可能需要某种形式的内部锁,所以该方法不能绝对保证线程不被阻塞。- 总的来说就是提供非阻塞的线程安全编程(提供线程的原子性).

原子变量类:

描述
AtomicBoolean可以用原子方式更新的 boolean 值。
AtomicInteger可以用原子方式更新的 int 值。
AtomicIntegerArray可以用原子方式更新其元素的 int 数组。
AtomicIntegerFieldUpdater基于反射的实用工具,可以对指定类的指定 volatile int 字段进行原子更新。
AtomicLong可以用原子方式更新的 long 值。
AtomicLongArray可以用原子方式更新其元素的 long 数组。
AtomicLongFieldUpdater基于反射的实用工具,可以对指定类的指定 volatile long 字段进行原子更新。
AtomicMarkableReferenceAtomicMarkableReference 维护带有标记位的对象引用,可以原子方式对其进行更新。
AtomicReference可以用原子方式更新的对象引用。
AtomicReferenceArray可以用原子方式更新其元素的对象引用数组。
AtomicReferenceFieldUpdater基于反射的实用工具,可以对指定类的指定 volatile 字段进行原子更新。
AtomicStampedReferenceAtomicStampedReference 维护带有整数“标志”的对象引用,可以用原子方式对其进行更新。

(2)常用方法摘要

返回类型方法描述
booleancompareAndSet(boolean expect, boolean update)如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值
intget()返回当前值。
voidset(boolean newValue)无条件地设置为给定值。
booleanweakCompareAndSet(boolean expect, boolean update)如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。
intgetAndIncrement()当前数值自增1

(3)简单使用示例

示例一:原子更新基本类型类 AtomicLong

public class Example1 {
 
    private final AtomicLong sequenceNumber = new AtomicLong(0);
    public long next() {
        //原子增量方法,执行的是i++,所以需要在获取一次。
        sequenceNumber.getAndIncrement();
        return sequenceNumber.get();
    }
 
    public void radixNext(int radix){
        for (;;) {
            long i = sequenceNumber.get();
            // 该方法不一定执行成功,所以用无限循环来保证操作始终会执行成功一次。
            boolean suc = sequenceNumber.compareAndSet(i, i + radix);
            if (suc) {
                break;
            }
        }
    }
 
 
    public static void main(String[] args) {
        Example1 sequencer = new Example1();
 
        //生成序列号
        for (int i = 0; i < 10; i++) {
            System.out.println(sequencer.next());
        }
 
        //生成自定义序列号
        for (int i = 0; i < 10; i++) {
            sequencer.radixNext(3);
            System.out.println(sequencer.sequenceNumber.get());
        }
 
 
    }
 
}

执行结果:

1
2
3
4
5
---------------
8
11
14
17
20

示例二:原子方式更新数组AtomicIntegerArray

public class Example2 {
 
    static AtomicIntegerArray arr = new AtomicIntegerArray(10);
 
    public static class AddThread implements Runnable{
        public void run(){
            for(int k=0;k<10000;k++){
                // 以原子方式将索引 i 的元素加 1。
                arr.getAndIncrement(k%arr.length());
            }
 
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        Thread[]ts=new Thread[10];
        //创建10个线程
        for(int k=0;k<10;k++){
            ts[k] = new Thread(new AddThread());
        }
 
        //开启10个线程
        for(int k=0;k<10;k++){
            ts[k].start();
        }
 
        //等待所有线程执行完成
        for(int k=0;k<10;k++){
            ts[k].join();
        }
 
        //打印最终执行结果
        System.out.println(arr);
    }
}

执行结果:

[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]复制代码

示例三:原子方式更新字段 AtomicReferenceFieldUpdater

// 创建对Person对象的name属性的原子变更类
AtomicReferenceFieldUpdater<Person, String> fieldUpdater = AtomicReferenceFieldUpdater.newUpdater(Person.class, String.class, "name");
 
Person person = new Person();
fieldUpdater.set(person, "jack");
 
// 原子性的将name属性将原来的jack转变为lucy
boolean b = fieldUpdater.compareAndSet(person, "jack", "lucy");
String s = fieldUpdater.get(person);
Assert.assertEquals("lucy", s);
 

public class Person {
    volatile String name;
    volatile int age;

}

使用AtomicReferenceFieldUpdater要注意的是,需要修改的类的字段必须是volatile修饰的,并且不能为私有的!!!

原子方式更新引用有AtomicReference,但是这个原子类也存在ABA问题,所以就出现了另外两个类来解决ABA的问题,就是AtomicStampedReference和AtomicStampedReference,这里聊聊AtomicStampedReference.

AtomicStampedReference怎么解决ABA问题呢?还是老办法在更新值的时候带上一个版本号(时间戳)用来识别值是否被更改过了

AtomicStampedReference<String> stamp = new AtomicStampedReference("abc",0);

在创建这个对象的时候,构造器传入一个引用变量和一个版本号,在修改数据的同时也修改后面的版本号就好了.

stamp.getStamp();//获取当前修改对象的版本号

//对比值,如果是abc就修改为bcd,然后对比版本号判断是否可以修改,修改完后版本号+1

stamp.compareAndSet("abc","bcd",stamp.getStamp(),stamp.getStamp()+1);

通过版本号这个方法就解决了ABA的问题.

(4)小结

原子访问和更新的内存效果一般遵循以下可变规则中的声明:

  • get 具有读取 volatile 变量的内存效果。
  • set 具有写入(分配)volatile 变量的内存效果。
    除了允许使用后续(但不是以前的)内存操作,其自身不施加带有普通的非 volatile 写入的重新排序约束,lazySet 具有写入(分配)volatile 变量的内存效果。在其他使用上下文中,当为 null 时(为了垃圾回收),lazySet 可以应用不会再次访问的引用。
  • weakCompareAndSet 以原子方式读取和有条件地写入变量但不 创建任何 happen-before 排序,因此不提供与除 - weakCompareAndSet 目标外任何变量以前或后续读取或写入操作有关的任何保证。
  • compareAndSet 和所有其他的读取和更新操作(如 getAndIncrement)都有读取和写入 volatile 变量的内存效果。

1-7等待机制

1)等待通机制

在多线程编程中,可能部分线程条件A只是暂时的没有满足(false),或许会有其他线程B更新A的条件使A条件满足.

可以将A线程暂停,直到B达到了他的条件,A就被唤醒,继续执行下一轮操作.

2)线程wait/notify 暂停/唤醒

如果想暂停一个线程可以使用Object类中的wait方法暂停 当前锁对象.wait(),然后会释放当前的锁对象

如,现在第1个线程执行A方法,num不等于0的话就会被暂停,

public synchronized void A(){
    //如果这个num不是0,就暂停线程,但是wait之前的代码正常执行
    if(num != 0){
        System.out.println("wait前面代码");
        //使用当前的锁对象.wait来暂停
        this.wait();
        //线程后面的代码将暂时不会执行,等待唤醒后继续执行
        System.out.println("wait后面代码");
    }


}

有时候我们希望线程在规定的时间内自动被唤醒则可以使用wait(long)方法,long则为唤醒的时间,

如this.wait(2000)则在两秒后自动唤醒.

如果想使暂停的线程被唤醒(进入准备阶段)则使用Object类中的notify()

如,现在有个线程2,我去执行一个B方法,同时唤醒一个A线程

public synchronized void B(){
    //如过num等于0则唤醒一个线程
    if(num == 0){
    //随机唤醒一个线程,当前只有1线程被暂停则唤醒1线程
    this.notify();
    }

}

注意:

1.notify只能随机唤醒一个被暂停的线程,如果只有一个就是唤醒那一个,如果有多个则是随机唤醒一个,并且notify执行唤醒后不会立即释放锁对象,而是等当前线程执行结束后释放锁,所以通常将notify放在代码块末尾执行.

2 .wait和notify要配对象锁使用,如果对象锁不同,或者在没有暂停线程的时候使用notify则会抛出异常

3.暂停和唤醒这两个方法只能在同步代码块中使用,否则将抛出异常.

3)notifyAll 唤醒所有线程

notify只能随机唤醒一个线程,那在有多个线程暂停的时候就要用到notifyAll()这个方法,比如我现在有三个线程暂停,我可以使用一个notifyAll方法将所有暂停线程全部唤醒,进入准备状态.

4)生产者-消费者模式

在多线程中,可以把线程分成两种,第一种是获取值的线程,第二种则是使用值的线程,好比现实生活中的生产者和消费者,生产者将东西生产出来,然后顾客将东西拿走.

在线程中有一组线程是负责获取数据的线程(生产者),而另一组线程读取并使用数据(消费者).

但是在多线程中,线程是无序的,并且操作是非原子性的,所以我们要用方式来控制这种模式

如果现在(消费者)线程需要一个数据,则必须要(生产者)线程去获取一个数据,所以(消费者)线程要等待(生产者)线程获取数据,有了这个思路我们就可以来写示例的代码

先看一个一生产者和一消费者的示例:

public class tes {
    //指定获取多少个值
    private int MAX = 1;

    //定义的list数组来存储数值,数组大小由MAX定义
    private List list = new ArrayList(MAX);

    //生产着的生产方法
    public synchronized void addl() throws InterruptedException {
            //当数组的长度大于或等于MAX的时候,说明不用产生数据了,则进入停止状态,等待唤醒
            while (list.size() >= MAX) {
                System.out.println("等待生产一个数值 " + Thread.currentThread().getName());
                this.wait();
                System.out.println("准备完毕,开始生产");
            }
            //如果需要生产值,则开始添加数值
            list.add(Math.random() * 100);
            System.out.println("添加完毕,数值为:" + list.get(0));
            //唤醒消费者获取数值
            this.notifyAll();
        }

    //消费者使用方法
    public synchronized void us() throws InterruptedException {
            //如果数组为0,则表示没有值可以读取,则进入停止状态,等待生产者生产数据
            //为什么用while不用if,因为如果开启多个线程的话可能会抛出数组越界异常
            while (list.size() == 0) {
                System.out.println("等待读取数值 " + Thread.currentThread().getName());
                this.wait();
                System.out.println("开始读取");
            }
            //如果有则将数据拿出来读取或使用,然后唤醒生产线程
            System.out.println("拿到的是:" + list.remove(0));
            this.notifyAll();
        }

}

主类实现线程:

public class tttt {


    public static void main(String[] args) throws InterruptedException {
        //创建一个方法类
        tes tes = new tes();
        //将这个方法类丢到生产/消费者线程类中使用
        sc sc1 = new sc(tes);
        sc sc2 = new sc(tes);
        sc sc3 = new sc(tes);
        xf xf1 = new xf(tes);
        xf xf2 = new xf(tes);
        xf xf3 = new xf(tes);
        //开启生产/消费线程
        sc1.start();
        sc2.start();
        sc3.start();
        xf1.start();
        xf2.start();
        xf3.start();


    }
}

//生产者线程类,消费者同样,但是使用方法为消费者方法
public class sc extends Thread {
//接收传入的方法类
    private Object obj;

    public sc(Object obj) {
        this.obj = obj;
    }

    @Override
    public void run() {
        try {
            while (true)
                //向下转型,使用生产者方法
            ((tes)obj).addl();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

当所有线程开始时,ArrayList数组里并没有数据,所以不管消费者是否先抢到执行权,也会进入等待状态而让出执行权,所以会被生产者线程抢到,当生产者抢到执行权后就开始生产数据,生产完毕后进入等待状态,而之后的生产线程则会进入等待状态,同时,消费者线程被唤醒,当一个消费者线程拿到执行权后就将数据拿出来,数组里面再次为空,然后使用数据,这样循环下去.

为什么使用notifyAll而不使用notify?

因为当多个生产和消费线程运行被暂停时,notify只能随机唤醒一个线程,并且可能是已经执行过操作的线程,比如:

生产者1拿到了执行权,然后开始生产数据,后面的生产者2也拿到了执行权,但是已经有数据了,所以生产者2进入等待状态,而生产者1执行完代码后又拿到了执行权,但是也因为有数据在等候

然后消费者1拿到了执行权,读取出数据后去唤醒了生产者2,而此时已经没有数据了,生产者2于是又去生产了一条数据,生产完后使用notify去唤醒线程,结果唤醒了生产者1线程,结果又添加了一条数据,但是数组的大小只能存放一个数据,所以会抛出数组越界的异常,而消费者线程也是如此,可能会多读一次数据也导致数组越界的异常.

所以我们要使用notifyAll去唤醒所有的线程

为什么判断不用if而去使用while?

接上面话题继续,我们唤醒了所有的线程,但是还是有问题,因为我们如果唤醒所有线程,则还是会向下执行,所以我们唤醒后还要判断当前是否符合条件执行我们的代码,所以使用while进行循环判断,如果符合(为false)则跳出循环,如果不符合(为true)则继续等待.

1-8 ThreadLocal

1) ThreadLocal的使用

多线程中,解决线程安全问题可以使用synchronized来解决安全问题,但是synchronized的效率却很低,所以引出ThreadLocal使用

现在有一个班的100个学生需要填写表格,我们如果要有序的填写则要提供一只笔(共享资源),而这个笔则成为每个人的争抢的对象,只能一个个的填写,这就是synchronized的模式,每个人都想填,但是只有一只笔,所以大家都需要去抢这支笔,而我们也可以给每个学生一只笔来填写,这样就不用一个个等待前面的学生填写完才能填写,同时可以更快的填写完毕,这就可以用到ThreadLocal来解决.

除了控制资源访问(synchronized),还可以通过增加资源(ThreadLocal)来保证线程安全.

ThreadLocal主要解决为每个线程绑定自己的值

public class Test01{
        
  //定义ThreadLocal对象
  static ThreadLocal tt = new ThreadLocal();

        //定义一个线程类
    static class Subthread extends Thread{

        public void run(){
            for(int i = 0; i < 20; i++){
            
                //设置和线程相关的值
                t1.set(Thread.currentThread().getName()+" - "+i);

                //打印tt调用get后的结果
                System.out.println(Thread.currentThread().getName()+" - "+tt.get());
           }
    

     }


 }



    public static void main(String[] args){
            //创建两个不同对象的线程
            Subthread t1 = new Subthread();
            Subthread t2 = new Subthread();
            //启动两个线程
              t1.start();
              t2.start();
        
        }

    }

得到的结果都是各自线程的值,所以ThreadLocal可以为每个线程保存自己的值,通过set来设置当前线程的值,get来返回当前线程的值.

而ThreadLocal其实是接收一个Object对象,我们也可以放进去一个对象

public class tttt {
            //通过泛型指定类型
    static ThreadLocal<SimpleDateFormat> loc = new ThreadLocal();

    static class thre {

        public void d() {
            try {
                if (loc.get() == null) {
                    //设置一个时间格式的对象
                    loc.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
                }
                String text = "2021-10-21 15:32:03";
                //得到对象并且使用里面的方法
                Date date = loc.get().parse(text);
                System.out.println(date);
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    }
}

2) ThreadLocal指定初始值

通常情况下,ThreadLocal被创建时里面的值返回一个null,如果要指定初始值,则要继承ThreadLocal并且重写ThreadLocal的initialValue方法.

                                       //表明返回一个Date类型的对象
public class Tests extends ThreadLocal<Date>{
        //重写initialValue设置一个初始值,返回一个Date对象
        protected Date initialValue(){
            //返回当前日期为初始值
            return new Date();
        }
    
    //而我们可以使用这个继承类来创建一个ThreadLocal对象
    //Tests t1 = new Tests();
}

1-9 Lock显示锁

在JDK5中增加了Lock锁接口,有ReentrantLock实现类,ReentrantLock锁称为可重入锁,它的功能比synchronized多.

锁的可重入性

当一个线程获取一个锁对象后还能继续获得多个锁对象,这就是锁的可重入性,synchronized就是一种可重入锁.

1) ReentrantLock显示锁使用

方法:

lock():获得锁

unlock():释放锁

演示用法:

public class tttt {
    //定义一个显示锁
    static Lock Need = new ReentrantLock();

    public static void us() {
        try {
            //首先获得锁
            Need.lock();
            //for循环就是同步代码块
            for (int i = 0; i < 3; i++) {
                System.out.println("当前线程是"+Thread.currentThread().getName()+"值为:"+i);
            }
        } finally {
            //释放锁
            Need.unlock();
        }
      
    }

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                us();
            }
        }).start();
        //第二个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                us();
            }
        }).start();
        //第三个线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                us();
            }
        }).start();
    }
}

结果:

当前线程是Thread-0值为:0
当前线程是Thread-0值为:1
当前线程是Thread-0值为:2
当前线程是Thread-1值为:0
当前线程是Thread-1值为:1
当前线程是Thread-1值为:2
当前线程是Thread-2值为:0
当前线程是Thread-2值为:1
当前线程是Thread-2值为:2

Process finished with exit code 0

其实和synchronized一样的是,线程必需要获得锁后才能执行同步代码块中的方法,但是可以看见锁的位置了,并且Lock也是可重入的锁.

3) lockInterruptibly()方法使用

lockInterruptibly()方法的作用:如果当前线程未被中断标记则可以获得锁,被中断标记了则放弃锁并且抛出异常.

如果在有需要中断的线程地方可以使用lockInterruptibly()代替lock()方法获取锁,这样可以随时中断线程并且不让线程继续竞争锁.

4)tryLock() 带参方法

tryLock(long time,TimeUnit unit)方法可以限时等待,在定义的时间内没有获取到锁,并且线程处于等待状态则返回一个false

tryLock(等待的时间,时间的单位),等一段时间,时间到了没拿到锁就返回一个false,拿到了就返回true

public class tttt {
    //非公平锁
    static ReentrantLock Need = new ReentrantLock();

    static class p implements Runnable {
        @Override
        public void run() {
            try {
                //尝试去获得锁,如果三秒内没拿到锁就不拿了
                if (Need.tryLock(3,TimeUnit.SECONDS)) {

                    Need.lock();
                   //休眠4秒,不会释放锁
                    Thread.sleep(4000);
                    for (int i = 0; i < 100; i++) {
                        System.out.println("线程"+Thread.currentThread().getName()+"正在打印"+i);
                    }
                }
                //锁我不要了
                else {
                    System.out.println("线程"+Thread.currentThread().getName()+"放弃了锁,我不要了!");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //判断当前线程是否持有锁对象,再决定是否能释放该锁
                if(Need.isHeldByCurrentThread()) {
                    System.out.println(Thread.currentThread().getName() + "释放了锁");
                    Need.unlock();

                }
            }

        }
    }

    public static void main(String[] args) {
        p p = new p();
        Thread t1 = new Thread(p);
        Thread t2 = new Thread(p);
        t1.start();
        t2.start();
    }

}

而tryLock()也可以不带参数,这样就更暴力了,线程进入到tryLock()拿锁,如果当前锁对象被其他线程持有,拿不到直接返回false,如果没有其他线程持有,拿到了则获得锁返回true,期间不会进行等待.

5)newCondition()方法

关键字synchronized与wait()/notify()这两个方法一起使用可以实现等待/通知模式,那Lock锁如果想实现则需要使用Lock的newContition()方法返回Condition对象,Condition类也可以实现等待/通知模式.

使用wait()/notify()时候,notify()不能指定唤醒一个线程,而是JVM虚拟机随机唤醒等待线程,但是使用Condition类可以进行选择性唤醒.

Condition类有两个方法来实现等待/唤醒,就是await()/signal()

await():会使当前线程等待,同时释放锁对象,当其他线程调用signal()时,线程会重新获得锁并继续执行剩下的代码.

signal():用来唤醒一个当前Condition对象等待队列中的线程.

注意:在调用Condition的await()/signal()方法之前,也需要线程持有相关的Lock锁,调用await()后线程会释放锁对象,在signal()调用后会从当前Condition对象的等待队列中唤醒一个线程.

public class tttt {
    //非公平锁
    static ReentrantLock Need = new ReentrantLock();
    //获得一个Condition对象
    static Condition cond = Need.newCondition();

    static class p implements Runnable {
        @Override
        public void run() {
            try {
                //获得锁
                Need.lock();
                System.out.println("线程" + Thread.currentThread().getName() + "准备开始等待");
                //暂停
                cond.await();
                System.out.println("线程" + Thread.currentThread().getName() + "结束了等待");

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //释放锁
                Need.unlock();
            }
        }
    }

    public static void main(String[] args) {
        p p = new p();
        Thread t1 = new Thread(p);
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //获得锁
                    Need.lock();
                    System.out.println("线程" + Thread.currentThread().getName() + "准备开始唤醒线程");
                    Thread.sleep(3000);
                    //唤醒线程
                    cond.signal();
                    System.out.println("线程" + Thread.currentThread().getName() + "结束了唤醒线程");

                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    //释放锁
                    Need.unlock();
                }
            }
        });
        t1.start();
        t2.start();
    }

}

运行结果:

线程Thread-0准备开始等待
线程Thread-1准备开始唤醒线程
线程Thread-1结束了唤醒线程
线程Thread-0结束了等待

Process finished with exit code 0

6)公平锁和非公平锁

前面学过synchronized和ReentrantLock锁,这两个锁都是非公平锁,所有线程随机竞争,可能导致线程饥饿和顺序不一致的问题,而公平锁则是按照执行顺序执行下去。

ReentrantLock提供了一个构造方法,可以将锁变为公平锁,在创建ReentrantLock对象的时候传入参数指定

ReentrantLock lo = new ReentrantLock(true);

这样lo锁就是公平锁,而线程也会按照先来后到的顺序执行。

7)Lock锁的一些常用方法

Lock锁的常用方法
返回值方法名作用
intgetHoldCount返回当前线程持有lock(锁)的次数
intgetQueueLength()返回正在等待获得锁的线程预估数
intgetWaitQueueLength(Condition condition)返回与Condition对象相关的等待线程预估数
booleanhasQueuedThread(Thread thread)查询参数指定的线程是否在等待获得锁
booleanhasQueuedThreads()查询是否还有线程在等待获得锁
booleanhasWaiters(Condition condition)查询是否还有线程在Condition等待队列等待
booleanisFair()判断是否为公平锁
booleanisHeldByCurrentThread()判断当前线程是否持有该锁对象
booleanisLocked()判断锁是否被线程持有

2-1读写锁(ReentranReadWriteLock)

基本简介

ReadWriteLock同Lock一样也是一个接口,提供了readLockwriteLock两种锁的操作机制,一个是只读的锁,一个是写锁。ReentranReadWriteLock是其实现类

读锁可以在没有写锁的时候被多个线程同时持有,可以有多个线程并发地读数据,写锁是独占的(排他的) 每次只能有一个写线程。

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

理论上,读写锁比互斥锁允许对于共享数据更大程度的并发。与互斥锁相比,读写锁是否能够提高性能取决于读写数据的频率、读取和写入操作的持续时间、以及读线程和写线程之间的竞争。
 

读写锁的使用场景

在一些共享资源的读和写操作,且写操作没有读操作那么频繁的场景下可以用读写锁
在没有写操作的时候,两个线程同时读一个资源没有任何问题,所以应该允许多个线程能在同时读取共享资源。但是如果有一个线程想去写这些共享资源,就不应该再有其它线程对该资源进行读或写。这就需要一个读/写锁来解决这个问题

互斥原则:

读-读能共存(即可以用多个线程同时的读)
读-写不能共存(即读的时候不能有 其他线程去修改,或者修改的时候不能有其他线程去读)
写-写不能共存(即修改的时候不能再有其他线程去修改)
 

读写锁简单理解:只要在读的时候就不能有线程在写,有一个线程在写就不能有其他线程读取或写入数据。

使用示例:

public class ReadWriteLockTest {
    public static void main(String[] args) {
        MyData myData = new MyData();
        for (int i = 1; i <= 5; i++) {
            final int key = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    myData.put(key + "", String.valueOf(key));
                }
            }, "t"+i).start();
        }

        for (int i = 6; i <= 10; i++) {
            final int key = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String s = myData.get((key - 5) + "");
                }
            }, "t"+i).start();
        }
    }
}

class MyData{
    private volatile Map<String ,String> map = new HashMap<>();
    // 可重入的读写锁
    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public void put(String key,String value) {
        try {
            readWriteLock.writeLock().lock();
            System.out.println(Thread.currentThread().getName() + " 正在写入");
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            map.put(key,value);
            System.out.println(Thread.currentThread().getName() + " 写入完成");
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

    public String get(String key) {
        try {
            readWriteLock.readLock().lock();
            System.out.println(Thread.currentThread().getName() + " 正在读取");
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            String v = map.get(key);
            System.out.println(Thread.currentThread().getName() + " 读取完成,读到数据:" + v);
            return v;
        } finally {
            readWriteLock.readLock().unlock();
        }
    }
}

结果显示:

t1 正在写入
t1 写入完成
t2 正在写入
t2 写入完成
t3 正在写入
t3 写入完成
t4 正在写入
t4 写入完成
t5 正在写入
t5 写入完成
t6 正在读取
t7 正在读取
t8 正在读取
t9 正在读取
t10 正在读取
t6 读取完成,读到数据:1
t10 读取完成,读到数据:5
t7 读取完成,读到数据:2
t9 读取完成,读到数据:4
t8 读取完成,读到数据:3

由此可以看出写只能一个个线程进行,而读取可以多个线程并发。

2-1线程池

1)什么是线程池?

1.理解
打个比方,正常的线程工作任务结束后,线程就会被销毁,想要再次使用就需要再次创建,而线程池不一样,在这个线程池里维护着数个线程,每次需要执行任务的时候就可以将任务提交到线程池,然后放入到阻塞队列中,等待线程执行任务,而线程执行完任务以后就会回到线程池中.从阻塞队列获取下一个任务这样往复运作,大大的提高了线程的效率,同时也减小了资源的占用.

2.为什么使用线程池?
使用线程池最大的原因就是可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行运行压力;当然了,使用线程池的原因不仅仅只有这些,我们可以从线程池自身的优点上来进一步了解线程池的好处;

3.使用线程池有哪些优势?
1:线程和任务分离,提升线程重用性;
2:控制线程并发数量,降低服务器压力,统一管理所有线程;
3:提升系统响应速度,假如创建线程用的时间为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间;

4.线程池应用场景介绍
应用场景介绍
1:网购商品秒杀
2:云盘文件上传和下载
3:12306网上购票系统等
总之
只要有并发的地方、任务数量大或小、每个任务执行时间长或短的都可以使用线程池;
只不过在使用线程池的时候,注意一下设置合理的线程池大小即可;

5.JDK提供与线程池相关的API

//Executor 线程池顶级接口 方法:execute(提交任务)
//ExecutorService/ThreadPoolExecutor 增加方法:submit(callable):Future,shutdown
//ScheduledExecutorService/ScheduledThreadPoolExecutor 增加了一些调度方法,不多概述
//Executors 线程池的静态工厂
ExecutorService ex = Executors.newSingleThreadExecutor();
//从静态工厂获取不同类型的线程池
ExecutorService ef = Executors.newFixedThreadPool();
Thread t1 = new MyThread();
Thread t2 = new MyThread();
Thread t3 = new MyThread();
Thread t4 = new MyThread();
Thread t5 = new MyThread();
ex.execute(t1);
ex.execute(t2);
ex.execute(t3);
ex.execute(t4);
ex.execute(t5);
//关闭线程池
ex.shutdown();

Executor中文意思执行器,它只有一个execute方法用来执行runnable任务。
所以Executor就是用来专门执行任务的执行器。jdk为我们提供了一个ExecutorService

ExecutorService继承了Executor接口,并且进行了增强。提供了一系列线程执行生命周期的方法和submit方法,该方法可以执行一个runnable任务,并且有一个返回值。返回类型为Future。

以上了解就行,不用过多深入.其实从线程静态工厂Executors获得的线程,底层获取都是使用new ThreadPoolExecutor获取的这个对象,而ThreadPoolExecutor则还有底层,往后会学习到,所以我们可以自己来定义ThreadPoolExecutor里面的参数来定义一个线程池执行我们的代码.

2)线程池的使用和方法

线程池的真正实现类是 ThreadPoolExecutor,其构造方法有如下4种:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}
 
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;

}

可以看到,其需要如下几个参数(重要!!!!):

  • corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
  • maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
  • keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
  • unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
  • workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
  • threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
  • handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。

线程池的使用流程如下:

// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(最大核心线程数,最大总线程数,非核心线程存活时间,时间单位,阻塞队列模式,线程工厂,拒绝策略);
// 向线程池提交任务
threadPool.execute(new Runnable() {
    @Override
    public void run() {
        ... // 线程执行的任务
    }
});
// 关闭线程池
threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程
threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表

3)阻塞队列模式

想执行任务,要向线程池提交任务会先判断是否达到最大核心线程数,如果没有则创建核心线程运行任务,如果核心线程满了,则放入阻塞队列中去.

阻塞队列有下面三种模式:

直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败(意思就是它的阻塞队列界限为0不存放任何任务),因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务(最大核心线程数为最大)。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize的值也就无效了。)当每个任务完全独立于其他任务,任务执行互不影响时,适合于使用无界队列;例如,在 Web页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

有界队列。当使用有限的 maximumPoolSizes时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。  

4) ThreadFactory线程工厂

ThreadFactory是一个接口,只有一个用来创建线程的方法Thread newThread(Runnable r),线程池中的线程都是由这个线程工厂创建出来的,如果我们没有选定则使用默认的线程工厂.

同时也可以自己定义一个线程工厂:

ThreadPoolExecutor ex = new ThreadPoolExecutor(1,1,5,Time.Unit.SECOND, new LinkBlockQueue<>(),new ThreadFactory(){

//重写该接口的newThread()方法
public Thread newThread(Runnable r){
//拿到r这个任务创建一个线程执行该任务
Thread t = new Thread(r);
}

});

通过重写了ThreadFactory接口中的newThread方法来创建一个自定义的线程.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值