1、线程与进程是什么
进程:是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。
线程:单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位。
2、线程能做什么
一个进程中可以有多个线程,也就是说,在一个进程运行时,会有很多属于这个进程的线程为其干活,而计算机中的线程是采用时间片轮转的方式来获取时间片,如线程A得到了时间片执行了0.03秒,线程A暂停,线程B得到了时间片,执行了0.001秒,时间片在分给别的线程。因此给用户造成了一个假象,就仿佛是几个任务在同时运行一样。这样的好处是,如果一个任务极其占用时间,会阻塞其他任务,因此采用这种方案可以防止堵塞,也可以增加CPU的利用率。
3、举个例子
用QQ打个比方,我们说QQ主程序是一个进程,这个进程中包含聊天线程,下载线程,视频线程,用户在使用的时候,可以后台下着软件的同时聊着天。
再比如,我们使用IDEA写代码的时候,会自动提示,自动判断语法错误,也就是说,在我们敲代码的同时,后台还有提示以及差错线程在为我们服务。
我们都知道ArrayList是线程不安全的,可能有点人说,我平时用ArrayList用的挺好啊, 没发现什么不对啊, 但是当你用多个线程同时操作ArrayList的时候,就会发现结果是不正确的,有时候ArrayList里面存储的是NULL,有的时候会少存储一个,甚至会报ConcurrentModificationException异常。
那么ArrayList不安全谁安全呢,肯定有人说Vector啊,Vector是线程安全的,没错,我们深究底层,发现Vector的构造方法调用this(10)重载构造方法,说明Vector初始大小是10,而体现在线程安全的代码如下图所示。
Vector添加方法
不出所料,就是加了个synchronized。那么我们在对比下ArrayList的添加源码。
ArrayList添加方法
果然ArrayList没有被Vector添加方法修饰。
Vector是ArrayList的上一代,Vector牺牲了性能保证了安全性,ArrayList牺牲了安全性保证了性能。
其实还有一种方案,就是通过Collections.synchronizedList(new ArrayList)
将ArrayList转换成线程安全的对象。
那上面的方案看起来换汤不换药,那有没有更好的方案去解决高并发呢,首先给大家引入一个重要概念读写分离,写时复制
读写分离,写时复制听起来好像不太好懂,我们拿上课签到举例子:
需求:现在有一张空名单,来的人要将自己的名字写上去完成签到。
方案一:采用线程不安全的方案
小三正在纸上写着自己的名字,由于想签到的同学(线程)太多了,没有老师来限制他们,他们就疯的一般上前去签到,小三刚刚写完 “ 小 ” 字,就被被人抢走了,导致数据出现错误。
方案二:采用加锁方案
小三正在纸上写着自己的名字,由于想签到的同学(线程)太多了,每个人都想先签到,但是老师让他们在门外等着,只有小三签到以后他们才能签到。也就是说小三签到的时候,别人看不到已签到名单(写则不能读)。
方案三:读写分离,写时复制。
我们都知道,只有写操作才会对资源造成修改,但是读不会对资源造成修改,所以我们就将已签到名单粘在墙上,同学们都可以看墙上的名单(并发读),当有同学要签到的时候,将墙上的名单复制(别管怎么复制,这只是例子,他有超能力)一份,把自己的名字写在复制版名单上(加锁写),在此期间并没有影响其他同学读名单,然后在将墙上的替换掉。这就是读写分离,写时复制。
CopyOnWriteArrayList就是采用这种思想,在实际实现中,我们发现只需要将List list = new ArrayList();
替换成List list = new CopyOnWriteArrayList();
之后就可以解决高并发产生的问题。
但是我们不知仅仅满足解决这个问题,也不能满足仅仅懂得读写分离,写时复制的思想,我们还应该了解CopyOnWriteArrayList的底层源码。
CopyOnWriteArrayList 构造方法
根据注释以及代码,我们知道,构造方法实际上就是创建一个空的Object[0]的数组。
CopyOnWriteArrayList的代码很容易理解,如下图所示。
我们发现CopyOnWriteArrayList实现了List接口,因此可以使用List接口回调。我们还发现了lock,这是JUC里面的知识,详见线程进阶第三章,简单的说就是一把锁,还是有一个是Object修饰的一个数组。
CopyOnWriteArrayList基本的组成部分我们看了,再看看关键的add()方法。
CopyOnWriteArraylist add方法
当有线程抢到这把锁之后.
lock.lock();
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len+1);
newElements[len] = e;
setArray(newElements);
lock.unlock();
做个总结:
Copyoniwrite容器即写时复制的容器。
往一个容器添加元素的时候,不直接往当前容器object[]添加,而是先将当前容器object[]进行copy复制出一个新的容器object[ ] newELements,然后在新的容器object[ ] newELements里添加元素,添加完元素之后,再将原容器的引用指向新的容器setArray (newELements);。
这样做的好处是可以对CopyoOnwrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以copyonwrite容器也是一种读写分离的思想,读和写不同的容器
扩展
顺带提一下HashSet,HashSet的底层是HashMap,丢进去的值就是Key,Value是固定的一个Object常量,名字叫PERSENT。
我们举一反三,既然ArrayList可以通过Collections.synchronizedList(new ArrayList)
转换成线程安全。
那么Collections.synchronizedList()
是否同样适用于HashSet呢?答案是可以的。
那么Collections.synchronizedList()
是否同样适用于HashMap呢?答案是可以的。
那按照读写分离的方式有CopyOnWriteArrayList
,那有没有CopyOnWriteArraySet
呢,答案也是有的。
HashMap比较特殊,他的并发类是 ConcurrentHashMap<>();
资源类(仅供参考)
class Phone // Phone.java -> Phone.class -> load... JVM里面形成模板Class
{
public static synchronized void sendEmail() throws Exception
{
TimeUnit.SECONDS.sleep(4);
System.out.println("-----sendEmail");
}
public synchronized void sendSMS() throws Exception
{
System.out.println("-----sendSMS");
}
public void sayHello()
{
System.out.println("-----sayHello");
}
}
主类(仅供参考)
public class Lock8
{
public static void main(String[] args) throws Exception
{
Phone phone = new Phone();//this1
Phone phone2 = new Phone();//this2
new Thread(() -> {
try
{
phone.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"A").start();
Thread.sleep(100);
new Thread(() -> {
try
{
//phone.sendSMS();
//phone.sayHello();
phone2.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"B").start();
}
}
题目:多线程8锁
1、 标准访问,当邮件和短音都有锁的时候,请问先打印邮件还是短信?
答:先打印邮件,后打印短信
结论:一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
2、 邮件新增暂停4秒钟的方法,请问先打印邮件还是短信?
答: 先打印邮件后打印短信。
结论: 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
3 、 新增普通的hello方法,请问先打印邮件还是hello
答:先打印邮件在打印hello
结论: 加个普通方法后发现和同步锁无关
4、 有两部手机,请问先打印邮件还是短信?
答:先打短信后打邮件
结论: 换成两个对象后,不是同一把锁了,情况立刻变化,谁先执行完就先打印谁,不冲突。
5、 两个静态同步方法,同一部手机,请问先打印邮件(静态)还是短信?
答:先打邮件后打短信,既谁先抢到谁就锁。
结论:见(6)
6 、两个静态同步方法,2部手机,请问先打印邮件还是短信?
答:先打邮件再打短信
结论: synchronized是实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式。
(1)对于普通同步方法,锁是当前实例对象等同于同步方法块。
(2) 对于同步方法块。锁的是Synchonized括号里配置的对象。
(3)对于静态同步方法,锁是当前类的Class对象本身,
7 、1个静态同步方法,1个普通同步方法,1部手机,请问先打印邮件(静态)还是短信?
答:先打短信在打邮件
结论: 见(8)
8 、1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信?
答:先打短信在打邮件
结论:所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class,具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的,但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后。
题目
现在两个线程,可以操作初始值为零的一个变量, 实现一个线程对该变量加1,一个线程对该变量减1,实现交替,来10轮,变量初始值为零。
资源类(仅供参考)
class AirConditioner//资源类
{
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public synchronized void increment()throws Exception
{
//1 判断(为了防止虚假唤醒应该用while)
if (number != 0)
{
this.wait();//A C
}
//2 干活
number++;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//3 通知
this.notifyAll();
}
public synchronized void decrement()throws Exception
{
//1 判断
if(number == 0)
{
this.wait();
}
//2 干活
number--;
System.out.println(Thread.currentThread().getName()+"\t"+number);
//3 通知
this.notifyAll();
}
}
主类(仅供参考)
public class ThreadWaitNotifyDemo
{
public static void main(String[] args)throws Exception
{
AirConditioner airConditioner = new AirConditioner();
new Thread(() -> {
for (int i = 1; i <=10; i++) {
try {
Thread.sleep(200);
airConditioner.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(() -> {
for (int i = 1; i <=10; i++) {
try {
Thread.sleep(300);
airConditioner.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
},"B").start();
在这个样例中,主类中使用两个线程去操作资源类是没有报错的,当线程超过3个时,结果会出现错误,这种现象就叫做虚假唤醒。
在这个流程中
在第一个步骤增一个。
第二个步骤,A线程结束,number=1,理想情况是 ‘ - ’ 进来。
但是在第三个步骤 A进程‘ + ’ 再次进来,此时发现number = 1, wait,此时有A这一个线程等待;
第四个步骤 B线程抢到锁, ‘ +’ ’ 进来了,发现number = 1,wait,此时有A、B两个线程等待。
第五个步骤 ‘ - ’ 进来了,消费一个,线程执行完毕,此时number = 0, 唤醒生产者。
生产者被唤醒之后直接将排队的A线程和B线程放行,此时number = 2,这就是虚假唤醒的由来。
解决虚假唤醒的办法是,将 if 改为while, 这样每次唤醒的时候,while会在将排队的线程 “拉”回去重新验证一次。
题目
多线程之间按顺序调用,实现A->B->C 三个线程启动,要求如下: AA打印5次,BB打印10次,CC打印15次接着 AA打印5次,BB打印10次,CC打印15次…来10轮
注意
资源类(仅供参考
class ShareResource
{
private int flag = 1;// 1:A 2:B 3:C
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void print5()
{
lock.lock();
try
{
//1 判断
while(flag != 1)
{
c1.await();// A系统就要停
}
//2 干活
for (int i = 1; i <=5; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//3 通知
flag = 2;
c2.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void print10()
{
lock.lock();
try
{
//1 判断
while(flag != 2)
{
c2.await();// A系统就要停
}
//2 干活
for (int i = 1; i <=10; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//3 通知
flag = 3;
c3.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void print15()
{
lock.lock();
try
{
//1 判断
while(flag != 3)
{
c3.await();// A系统就要停
}
//2 干活
for (int i = 1; i <=15; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//3 通知
flag = 1;
c1.signal();
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
主类(仅供参考)
public class ThreadOrderAccess
{
public static void main(String[] args)
{
ShareResource shareResource = new ShareResource();
new Thread(() -> {
for (int i = 1; i <=10; i++) {
shareResource.print5();
}
},"A").start();
new Thread(() -> {
for (int i = 1; i <=10; i++) {
shareResource.print10();
}
},"B").start();
new Thread(() -> {
for (int i = 1; i <=10; i++) {
shareResource.print15();
}
},"C").start();
}
}
题目
多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。 但是如果有一个线程想去写共享资源类,就不应该再有其它线程可以对该资源进行读或写。
注意
class MyCache
{
private volatile Map<String,String> map = new HashMap<>();
private Lock lock = new ReentrantLock();
public void put(String key ,String value)
{
System.out.println(Thread.currentThread().getName()+"\t 写入开始");
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"\t 写入结束");
}
public void get(String key)
{
String result = null;
System.out.println(Thread.currentThread().getName()+"\t 读取开始");
result = map.get(key);
System.out.println(Thread.currentThread().getName()+"\t 读取结束result: "+result);
}
}
主类(仅供参考)
public class ReadWriteLockDemo
{
public static void main(String[] args)
{
MyCache myCache = new MyCache();
for (int i = 1; i <=10; i++) {
int finalI = i;
new Thread(() -> {
myCache.put(finalI+"",finalI+"");
},String.valueOf(i)).start();
}
for (int i = 1; i <=10; i++) {
int finalI = i;
new Thread(() -> {
myCache.get(finalI+"");
},String.valueOf(i)).start();
}
}
}
这是一个错误的样例,可以看到,在资源类中,并没有对put和get进行锁限制,如果直接使用多个线程并发来进行put、get操作,会导致数据出现问题。
打个比方说,上课签到,理想状态是,同学们一人签到,在纸上签名,其他人在旁边看着。而没有加锁会出现小明正在写自己的名字,刚写到 “ 小 ” , “ 明 ” 还没来的及写,就被小刚抢走了。这样会出现数据不一致性,这是我们不愿意看到的。
这样的程序运行结果就是,会有人夹三
那我们如果对写与读加个锁呢 ?
class MyCache
{
private volatile Map<String,String> map = new HashMap<>();
private Lock lock = new ReentrantLock();
public void put(String key ,String value)
{
lock.lock();
try
{
System.out.println(Thread.currentThread().getName()+"\t 写入开始");
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"\t 写入结束");
}finally {
lock.unlock();
}
}
public void get(String key)
{
lock.lock();
try
{
String result = null;
System.out.println(Thread.currentThread().getName()+"\t 读取开始");
result = map.get(key);
System.out.println(Thread.currentThread().getName()+"\t 读取结束result: "+result);
}finally {
lock.unlock();
}
}
}
运行结果
还是原来的材料,还是原来的配方,只不过在get和put上各加了一把锁。可以看到,写入已经规范起来了,再也没有夹三的情况出现了,但是随之而来出现了个问题,我们理想中是不希望读被限制的,就像我们允许多个人同时看电影,显而易见,在运行结果中读也被限制了。这会让程序的运行效率变低。
我们都能发现的问题,先人肯定也是可以发现的,并且睿智的先人还为我们提供了解决方案那就是—ReentrantReadWriteLock,这是juc的一个类,它可以做到读写分离。
资源类(仅供参考)
class MyCache
{
private volatile Map<String,String> map = new HashMap<>();
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void put(String key ,String value)
{
readWriteLock.writeLock().lock();
try
{
System.out.println(Thread.currentThread().getName()+"\t 写入开始");
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"\t 写入结束");
}finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String key)
{
readWriteLock.readLock().lock();
try
{
String result = null;
System.out.println(Thread.currentThread().getName()+"\t 读取开始");
result = map.get(key);
System.out.println(Thread.currentThread().getName()+"\t 读取结束result: "+result);
}finally {
readWriteLock.readLock().unlock();
}
}
}
ReentrantReadWriteLock的用法很简单,可以在看到,写是有锁限制的,而读是没有限制的,这样可以最大的利用资源,这个版本才是较好的解决方案。
题目
我们在上学的时候,往往会遇到晚自习之后班长要等值日生都值日完毕才能关灯锁门。班长不可能先走,然后把值日生都锁在屋子里,而班长就是主线程,6个值日生就是其他线程。
主类(仅供参考)
public class CountDownLatchDemo
{
public static void main(String[] args) throws Exception
{
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <=6; i++) {
new Thread(() -> {
/*try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
System.out.println(Thread.currentThread().getName()+"\t离开教室");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await();
//countDownLatch.await(2L,TimeUnit.SECONDS);
System.out.println(Thread.currentThread().getName()+"\t 关门离开");
}
}
显而易见,如果不适用CountDownLacth的话,由于线程的执行速度快慢不一,所以主线程很可能会在所有线程执行完毕之后在结束,容易想到的方案就是让主线程sleep,但是一秒钟能想到的答案都是错误的,sleep会让程序执行效率变低,而且我们也不知道让它sleep多长时间,所以我们使用CountDownLacth,它可以设置主线程等待的时间。
题目
我们小的时候,经常会看龙珠这部动漫,众所周知,召唤神龙需要收集齐7颗龙珠,既当前线程需要等待所有相关线程需要全部准备完毕才能执行。
主类(仅供参考)
public class CyclicBarrierDemo
{
public static void main (String[] args)
{
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,() -> {
System.out.println("集齐7颗龙珠,能够召唤神龙");});
for (int i = 1; i <=7; i++) {
int finalI = i;
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName()+"\t收集到第:"+ finalI +"\t");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
题目
开车去超市最讨厌的是什么,没错就是抢车位,往往有很多车需要抢多个车位,如果没抢到那么就只能等待了,如果实在等不到只能去更远的 地方找车位或者直接回家了,既多个线程争抢多个资源。
主类(仅供参考)
public class SemaphoreDemo
{
public static void main(String[] args)
{
Semaphore semaphore = new Semaphore(3); //模拟3个停车位
for (int i = 1; i <=6; i++) {
new Thread(() -> {
boolean flag = false;
try
{
semaphore.acquire();
flag = true;
System.out.println(Thread.currentThread().getName()+"\t 抢到车位");
//暂停3秒钟线程
try {
TimeUnit.SECONDS.sleep(new Random().nextInt(5)); } catch (InterruptedException e) {
e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t -------离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
if(flag)
{
semaphore.release();
}
}
},String.valueOf(i)).start();
}
}
}
大学的时候我们耳熟能详,生产一个线程需要继承Thread类或者实现Runnable接口。而现如今我们应该知道,在JAVA中尽量少的使用继承,因为继承很宝贵,JAVA是一个仅仅支持单继承的语言(C++支持多继承)。所以我们应该尽量的使用继承接口的方式既实现Runnable接口。
随着业务的发展,慢慢的,JAVA1.1时出现的Runnable已经无法满足业务需要了,进而在1.5版本出现了Callable,那么Callable比Runnable好在哪里呢。
在这里我们举一个例子,一个老师给50个学生上课,上课中途,老师渴了,需要喝水。有两种解决方案,一种是老师亲自去买水,来回时间需要5分钟,另一种方案就是老师托人去买水,自己继续讲课。
上面这个例子能很容易的看出来,买水是一件很浪费时间的事情,如果老师亲自去买水,那么50个同学(线程)都需要等待,这就是Runable的解决方案。
Callbale支持泛型,有返回值,显而易见,实现了Callable接口就可以派遣线程作复杂运算,从而不会使当前线程阻塞,这就是异步思想。
很显然,1.1时代的Runable和1.5版本的Callable没有一丝一毫的关系。那么怎么使用呢,众所周知,Thread只需要在参数中传入一个匿名内部类或者一个实现过Runnable接口的实现类就可以,建立一个线程。但是Thread没有Callable的构造方法。那么怎么办呢?
可以很容易想到,只需要有一类同时实现Runnable和Callable接口就可以了,因为这样Runnable和Callable就产生了联系,但是并不推荐这样使用。因为Runnable和Callable本质上都是做同一种事情的,就像是工作人员不会带两个工作牌一样。
spring有一个很厉害的思想就是构造注入,引用这个思想,那么能不能有一个类,继承自Runable接口而且将Callable作为一个构造参数从而使得Runable和Callable发生关系。
别说还真有这么一个类,这个类就是FutureTask,我们阅读源码可以发现,FutureTask其实是实现了RunableFuture< V >,而RunableFuture是继承自Runable接口。FutureTask有一个构造方法正需要Callable。
在这里,FutureTask作为一个中间人,它介绍了Ranable和Callable认识,从而使得他们产生关系,这就是适配器模式。下图是FutureTask的关系图。
参考类
public class LamdaCallable {
//get 方法一般放在最后一行
public static void main(String[] args) throws ExecutionException, InterruptedException {
int a = 1;
System.out.println("第一步:简单计算"+1);
FutureTask futureTask = new FutureTask(()->{
//callable
int temp = 10086;
Thread.sleep(1000);
System.out.println("第二步:分支线程做了一个很复杂的计算");
return temp;
});
int b = a+2;
System.out.println("第三步:简单的计算"+b);
new Thread(futureTask, "a").start();
int c = b + (int)futureTask.get();
System.out.println("第四步:计算完毕"+c);
}
}
在多线程领域:所谓阻塞,在某些情况下会挂起线程(既阻塞),一旦条件满足,被挂起的线程又会被自动唤醒。
为什么需要BlockingQueue?
好处是我们不需要关心什么时候阻塞线程,什么时候唤醒线程,因为着一些都被BlockingQueue一手操办了。
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和安全,这会给我们的程序带来不少复杂度。
阻塞队列是一个队列,数据结构如下图所示:
阻塞的产生有两种可能性,一种是由于程序的健壮性差导致的阻塞,另一种是由于业务需要所以进行阻塞,如生-消模型。
当队列是空的,从队列中获取元素的操作将会被堵塞。
当队列是满的,为队列添加元素的操作将会被阻塞。
在JAVA中,BlockingQueue实际上是一个接口
看到关系图后,我们发现BlockingQueue有很多的实现类,同样,BlockingQueue来自于Queue, Queue来自于Collection,而List和Queue是平级,正像是我们熟悉的ArrayLis,LinkedList一样,BlockingQueue也有ArrayBlockingQueue、LinkedBlockingQueue,所以BlockingQueue同样比较容易上手。
BlockingQueue实现类种类分析:
BlockingQueue实现类方法分析:
测试参考代码
public class BlockingQueueDemo
{
public static void main(String[] args) throws Exception
{
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue(3);
blockingQueue.put("a");
blockingQueue.put("a");
blockingQueue.put("a");
//blockingQueue.put("a");
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.take());
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("a"));
System.out.println(blockingQueue.offer("x"));
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.add("a"));
System.out.println(blockingQueue.add("b"));
System.out.println(blockingQueue.add("c"));
//System.out.println(blockingQueue.add("x"));
System.out.println(blockingQueue.element());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
}
}
众所周知,10年前的单核cup电脑是假的多线程,就像是马戏团小丑玩多个球,cup需要来回切换,现在是多和核电脑 ,多个线程各自跑在独立的cup上,不用切换,效率更高了。
线程池的优势
线程池主要做的工作是控制运行的线程数量,处理过程将线程任务放入队列,可以同时处理这些任务,如果线程数量超过了最大数量,超出的线程阻塞等候,等其他线程执行完毕,再从队列中取出任务来执行。就像是8个停车位,10个车,8个车进入到停车位停车,2个车等待。等其他车走了之后,他们再按照某种约定谁先占有停车位。
线程池的主要特点为线程复用、控制最大并发数、管理线程
java中的线程池主要是通过Executor框架实现的,该框架中用到了Executor、Executors、ExecutorService、ThreadPoolExecutor这几类。Executor架构图如下图所示:
线程池是多线程的重点,我对线程池的理解,java多线程主要就是ThreadPollExecutor类,现在混个耳熟,下面会详细解释。
就像是ArrayList一样,我们一般不直接New它,一般都用接口去承接它,如果想使用Java线程池我们最好也用ExecutorService来承接new出来的线程池对象。如以下代码所示:
参考代码
测试参考代码
public class MyThreadPoolDemo
{
public static void main(String[] args)
{
// ExecutorService executorService = Executors.newFixedThreadPool(5);//一池5线程
// ExecutorService executorService = Executors.newSingleThreadExecutor();//一池1线程
ExecutorService executorService = Executors.newCachedThreadPool();//一池N线程
try
{
for (int i = 1; i <=20; i++)//模拟20个客户来银行办理业务,提交请求。customer
{
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName()+"\t 办理业务"+new Random().nextInt(10));
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
executorService.shutdown();
}
}
}
这一段代码代码很简单,在参考代码中可以看到,声明了3个ExecutorService,只不过实现ExecutorService的类不太一样,他们分别是Executors.newFixedThreadPool(5),这代表声明一个线程池,里面放了5个线程,Executors.newSingleThreadExecutor(),这代表一池里面只能放一个线程,这两个例子就像是银行在工作日开了5个窗口,双休日开一个窗口一样,Executors.newCachedThreadPool(),就像他的名字一样,这是一个可以自动改变线程数量的线程池。有必要注意的是,请看清,在这个样例中是用Executors这个工具类的静态方法来实现创建线程池对象的,而非Executor。
这三个线程池优缺点各异
那么上面学了三种线程池的方案,那么用哪一种比较好呢?答案是一个不用
前面的是基础,如果你只会这些,那么不好意思,你只是一个API调用工程师
调用API也要知道这个API的大致实现源码吧!
我们简单粗暴的分析下上面说过的三个方法的源码。
newFixedThreadPool源码
newSingleThreadExecutor 源码
newCachedThreadPool源码
不知道大家是否记得,前面我说过一个叫做ThreadPoolExecutor类,让大家混混脸熟的。
大致一看,哇!发现他们都是通过 ThreadPoolExecutor 类的构造方法创建对象的,还有5个参数。
我们大致分析下这些构造方法
newFixedThreadPool
创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue(前面说过的阻塞队列)
newSingleThreadExecutor
创建的线程池corePoolSize和maximumPoolSize值都是1,它使用的是LinkedBlockingQueue(前面说过的阻塞队列)
newCachedThreadPool
创建的线程池从corePoolSize设置为0,将maximumPoolSize的值是Integer.MAX_VALUE,它使用的是SynchronousQueue,简单的说就是来了任务就执行,如果线程超过60秒那么就销毁线程。
其实当我们对比分析了这三个构造方法之后,大致上这5个参数的意思都猜出来了。总之,这三个产生线程池的方式都是通过ThreadPoolExecutor的构造方法创建的,那我们看看ThreadPoolExecutor的构造方法是怎么定义的,如图所示:
ThreadPoolExecutor构造方法定义
我们看了ThreadPoolExecutor的构造方法,发现这 5(重点) 个参数中还有一个this()参数,而这个this是将原有的5个参数打包,又新增了两个参数,现在是 7(重点) 个参数了。
带着好奇的心思,我们在看看this,原来这是一个重载的构造方法,如下图所示
ThreadPoolExecutor重载构造方法定义
我们发现,实际上,最终用到的有 **7(重点)**参数,而非前面见到的5个。
那么这么多参数都是什么意思呢?
int corePoolSize 线程池中的常驻核心数
int maximumPoolSize 线程池中能够容纳同时执行的最大线程数,必须大于1
long keepAliveTime 多余的空闲线程完成时间
当线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余的线程会销毁,直到剩下corePoolSize的数量。
TimeUnit unit keepAliveTime的单位
BlockingQueue wokeQueue 任务队列,被提交但尚未执行的任务。
ThreadFactory threadFactory 表示线程池的工作线程的线程工厂,用于创建线程,一般用默认的就行
RejectedExecutionHandler handler 拒绝策略,表示当前队列满了,并且工作线程大于等于线程池的最大参数(maxmumPoolSize)时如何来拒绝请求执行的runnable的策略
那么有了这些参数,具体的执行流程是怎么样呢? 如下图所示:
上图文字解释
execute()这个方法没见过?别担心,他的用法和前面见到的submit()大致一样。
原理了解了,那么我们用JDK自带的线程池工具重写一个卖票吧。
class Ticket//资源类
{
private int number = 30;
Lock lock = new ReentrantLock();
public void sale()
{
lock.lock();
try
{
if(number > 0)
{
System.out.println(Thread.currentThread().getName()+"\t卖出第:"+(number--)+"\t 还剩下:"+number);
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
/**
* 题目:三个售票员 卖出 30张票
* 目的:如何写出企业级的多线程程序
*
* ** 高内聚,低耦合
*
* 1 高内低耦的前提下,线程 操作 资源类
*
*/
public class SaleTicket
{
public static void main(String[] args) throws Exception //main一切程序入口
{
Ticket ticket = new Ticket();
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 1; i <=30; i++) {
executorService.submit(() -> {
ticket.sale();
});
}
executorService.shutdown();
}
}
这是一个很简单的案例,指示采用JDK自带的池化技术实现了卖票而已,前面也说了,这三个JDK自带的池化工具最好都不要用,虽然用起来很顺手,为什么不能用请看下章。
写这一章的原因可能有人是认为我想要对线程池的了解更深一点,实际上这只是一部分,在实际开发中,理论上必须要手写线程池。话不多说,我们看一下阿里的开发手册,如下图所示。
阿里的开发手册说的很清晰,如果用JDK自带的池化工具会导致OOM!可怕!我们用源码解释导致OOM的原因。
newFixedThreadPool 构造方法
这个不能再熟悉的构造方法中,有一个LinkedBlockingQueue的阻塞队列,用途在前面已经详细说过了,就类似于候客区,那么拿银行举个例子,银行的候客区一般是20个位子,大一点的银行是30-50个位子,这应该是极限了吧,那么如果某家银行的位子是Integer.MAX_VALUE这么多呢,如果真有这么多人来办理业务呢?想必这会把房子撑爆(OOM)吧!下面我们继续看源码:
LinkedBlockingQueue 构造方法
果然,默认大小是Integer_MAX_VALUE,而与之相反的newSingleThreadExecutor是只有一个位子,这种情况一般也不用,因此要自己手写线程池。想必看到这里大家就没有疑问了。
进入手写线程池的第一步,参考源码,我们可以知道,ThreadPoolExecutor是关键。如图
参考源码我们可以很容易的写出一个线程池,我们拿银行办理业务做例子。
public class MyThreadPoolDemo
{
public static void main(String[] args)
{
ExecutorService executorService = new ThreadPoolExecutor(
2,//coreSize - 常驻核心数
5,//maximumpoolsize - 最大数
2L,//keepAliveTime - 销毁非常驻线程等待时间
TimeUnit.SECONDS,//unit - 时间单位
new ArrayBlockingQueue<>(3),//BlockingQueue - 阻塞队列,大小为 3(注意)
Executors.defaultThreadFactory(),//threadFactory - 工厂
new ThreadPoolExecutor.AbortPolicy());//RejectedExecutionHandler - 拒绝策略
try
{
for (int i = 1; i <=10; i++)//模拟n个客户来银行办理业务,提交请求。customer
{
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName()+"\t 办理业务"+new Random().nextInt(10));
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
executorService.shutdown();
}
}
}
代码很容易理解,我的问题是,此线程池最多可以同时处理多少任务?如果太多的任务到来,线程池处理不了怎么办?
答案是,在本样例中,最多可以同时执行5+3=8个,公式如下
Γ ( m a x ) = m a x i m u m p o o l s i z e + B l o c k i n g Q u e u e . s i z e ( ) \Gamma(max) = maximumpoolsize+BlockingQueue.size() Γ(max)=maximumpoolsize+BlockingQueue.size()
那么如果太多的任务到来,线程池处理不了就会参考JDK内置的拒绝策略处理,在本样例的AbortPolicy(默认)会直接抛出RejectedExecutionException异常如下图所示,来阻止系统运行,思考可知这样是不合理的。
JAVA预置了4个常用的拒绝策略
我们上面已经理解了AbortPolicy,那么CallerRunsPolicy呢,看到官方介绍有些不清晰,看一下执行结果大家懂了。
CallerRunsPolicy 调用者回退
用过执行20个任务,我们发现,我们是通过main线程去调用线程池去处理任务的,线程池处理不了就把任务退回到调用者(main)线程执行,没有报异常。
DiscardPolicy会丢弃处理不了的任务,就像这样:
DiscardPolicy
在DiscardPolicy拦截模式下,执行了 10 个任务,默默的丢弃了2个, DiscardOldestPolicy与DiscardPolicy的区别只不过是丢弃等待最久的任务,就不做演示了。
学到这里,对多线程也就基本入门了,还有更多的东西要学。
第一次写博客,如有错误请批评指正,谢谢大家观看。