线程池 BlockingQueue synchronized volatile

前段时间看了一篇关于"一名3年工作经验的程序员应该具备的技能"文章,倍受打击。很多熟悉而又陌生的知识让我怀疑自己是一个假的程序员。本章从线程池,阻塞队列,synchronized 和 volatile关键字,wait,notify方法实现线程之间的通讯,死锁,常考面试题。将这些零碎的知识整合在一起。如下图所示。

学习流程图:



技术:Executors,BlockingQueue,synchronized,volatile,wait,notify

说明:文章学习思路:线程池---->队列---->关键字---->死锁---->线程池实战

源码:https://github.com/ITDragonBlog/daydayup/tree/master/ThreadBase

线程池

线程池,顾名思义存放线程的池子,可以类比数据库的连接池。因为频繁地创建和销毁线程会给服务器带来很大的压力。若能将创建的线程不再销毁而是存放在池中等待下一个任务使用,可以不仅减少了创建和销毁线程所用的时间,提高了性能,同时还减轻了服务器的压力。

线程池的使用

初始化线程池有五个核心参数,分别是 corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue。还有两个默认参数 threadFactory, handler

corePoolSize:线程池初始核心线程数。初始化线程池的时候,池内是没有线程,只有在执行任务的时会创建线程。

maximumPoolSize:线程池允许存在的最大线程数。若超过该数字,默认提示RejectedExecutionException异常

keepAliveTime:当前线程数大于核心线程时,该参数生效,其目的是终止多余的空闲线程等待新任务的最长时间。即指定时间内将还未接收任务的线程销毁。

unit:keepAliveTime 的时间单位

workQueue:缓存任务的的队列,一般采用LinkedBlockingQueue。

threadFactory:执行程序创建新线程时使用的工厂,一般采用默认值。

handler:超出线程范围和队列容量而使执行被阻塞时所使用的处理程序,一般采用默认值。

线程池工作流程

开始,游泳馆来了一名学员,于是馆主安排一个教练负责培训这名学员;

然后,游泳馆来了六名学员,可馆主只招了五名教练,于是有一名学员被安排到休息室等待;

后来,游泳馆来了十六名学员,休息室已经满了,馆主核算了开支,预计最多可招十名教练;

最后,游泳馆只来了十名学员,馆主对教练说,如果半天内接不到学员的教练就可以走了;

结果,游泳馆没有学员,关闭了。

在接收任务前,线程池内是没有线程。只有当任务来了才开始新建线程。当任务数大于核心线程数时,任务进入队列中等待。若队列满了,则线程池新增线程直到最大线程数。再超过则会执行拒绝策略。

线程池的三种关闭

shutdown: 线程池不再接收任务,等待线程池中所有任务完成后,关闭线程池。常用

shutdownNow: 线程池不再接收任务,忽略队列中的任务,尝试中断正在执行的任务,返回未执行任务列表,关闭线程池。慎用

awaitTermination: 线程池可以继续接收任务,当任务都完成后,或者超过设置的时间后,关闭线程池。方法是阻塞的,考虑使用

线程池的种类

1 newSingleThreadExecutor() 单线程线程池

初始线程数和允许最大线程数都是一,keepAliveTime 也就失效了,队列是无界阻塞队列。该线程池的主要作用是负责缓存任务。

2 newFixedThreadPool(n) 固定大小线程池

初始线程数和允许最大线程数相同,且大小自定义,keepAliveTime 也就失效了,队列是无界阻塞队列。符合大部分业务要求,常用。

3 newCachedThreadPool() 缓存无界线程池

初始线程数为零,最大线程数为无穷大,keepAliveTime 60秒类终止空闲线程,队列是无缓冲无界队列。适合任务数不多的场景,慎用。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; /**
* 线程池
* 优势,类比数据库的连接池
* 1. 频繁的创建和销毁线程会给服务器带来很大的压力
* 2. 若创建的线程不销毁而是留在线程池中等待下次使用,则会很大地提高效率也减轻了服务器的压力
*
* 三种workQueue策略
* 直接提交 SynchronousQueue
* 无界队列 LinkedBlockingQueue
* 有界队列 ArrayBlockingQueue
*
* 四种拒绝策略
* AbortPolicy : JDK默认,超出 MAXIMUM_POOL_SIZE 放弃任务抛异常 RejectedExecutionException
* CallerRunsPolicy : 尝试直接调用被拒绝的任务,若线程池被关闭,则丢弃任务
* DiscardOldestPolicy : 放弃队列最前面的任务,然后重新尝试执被拒绝的任务。若线程池被关闭,则丢弃任务
* DiscardPolicy : 放弃不能执行的任务但不抛异常
*/
public class ThreadPoolExecutorStu { // 线程池中初始线程个数
private final static Integer CORE_POOL_SIZE = 3;
// 线程池中允许的最大线程数
private final static Integer MAXIMUM_POOL_SIZE = 8;
// 当线程数大于初始线程时。终止多余的空闲线程等待新任务的最长时间
private final static Long KEEP_ALIVE_TIME = 10L;
// 任务缓存队列 ,即线程数大于初始线程数时先进入队列中等待,此数字可以稍微设置大点,避免线程数超过最大线程数时报错。或者直接用无界队列
private final static ArrayBlockingQueue<Runnable> WORK_QUEUE = new ArrayBlockingQueue<Runnable>(5); public static void main(String[] args) {
Long start = System.currentTimeMillis();
/**
* ITDragonThreadPoolExecutor 耗时 1503
* ITDragonFixedThreadPool 耗时 505
* ITDragonSingleThreadExecutor 语法问题报错,
* ITDragonCachedThreadPool 耗时506
* 推荐使用自定义线程池,或newFixedThreadPool(n)
*/
ThreadPoolExecutor threadPoolExecutor = ITDragonThreadPoolExecutor();
for (int i = 0; i < 8; i++) { // 执行8个任务,若超过MAXIMUM_POOL_SIZE则会报错 RejectedExecutionException
MyRunnableTest myRunnable = new MyRunnableTest(i);
threadPoolExecutor.execute(myRunnable);
System.out.println("线程池中现在的线程数目是:"+threadPoolExecutor.getPoolSize()+", 队列中正在等待执行的任务数量为:"+
threadPoolExecutor.getQueue().size());
}
// 关掉线程池 ,并不会立即停止(停止接收外部的submit任务,等待内部任务完成后才停止),推荐使用。 与之对应的是shutdownNow,不推荐使用
threadPoolExecutor.shutdown();
try {
// 阻塞等待30秒关掉线程池,返回true表示已经关闭。和shutdown不同,它可以接收外部任务,并且还阻塞。这里为了方便统计时间,所以选择阻塞等待关闭。
threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("耗时 : " + (System.currentTimeMillis() - start));
} // 自定义线程池,开发推荐使用
public static ThreadPoolExecutor ITDragonThreadPoolExecutor() {
// 构建一个,初始线程数量为3,最大线程数据为8,等待时间10分钟 ,队列长度为5 的线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.MINUTES, WORK_QUEUE);
return threadPoolExecutor;
} /**
* 固定大小线程池
* corePoolSize初始线程数和maximumPoolSize最大线程数一样,keepAliveTime参数不起作用,workQueue用的是无界阻塞队列
*/
public static ThreadPoolExecutor ITDragonFixedThreadPool() {
ExecutorService executor = Executors.newFixedThreadPool(8);
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
return threadPoolExecutor;
} /**
* 单线程线程池
* 等价与Executors.newFixedThreadPool(1);
*/
public static ThreadPoolExecutor ITDragonSingleThreadExecutor() {
ExecutorService executor = Executors.newSingleThreadExecutor();
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
return threadPoolExecutor;
} /**
* 无界线程池
* corePoolSize 初始线程数为零
* maximumPoolSize 最大线程数无穷大
* keepAliveTime 60秒类将没有被用到的线程终止
* workQueue SynchronousQueue 队列,无容量,来任务就直接新增线程
* 不推荐使用
*/
public static ThreadPoolExecutor ITDragonCachedThreadPool() {
ExecutorService executor = Executors.newCachedThreadPool();
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
return threadPoolExecutor;
} } class MyRunnableTest implements Runnable {
private Integer num; // 正在执行的任务数
public MyRunnableTest(Integer num) {
this.num = num;
}
public void run() {
System.out.println("正在执行的MyRunnable " + num);
try {
Thread.sleep(500);// 模拟执行事务需要耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("MyRunnable " + num + "执行完毕");
}
}

队列

队列,是一种数据结构。大部分的队列都是以FIFO(先进先出)的方式对各个元素进行排序的(PriorityBlockingQueue是根据优先级排序的)。队列的头移除元素,队列的末尾插入元素。插入的元素建议不能为null。Queue主要分两类,一类是高性能队列 ConcurrentLinkedQueue;一类是阻塞队列 BlockingQueue。本章重点介绍BlockingQueue

ConcurrentLinkedQueue

ConcurrentLinkedQueue性能好于BlockingQueue。是基于链接节点的无界限线程安全队列。该队列的元素遵循先进先出的原则。不允许null元素。

BlockingQueue

ArrayBlockingQueue: 基于数组的阻塞队列,在内部维护了一个定长数组,以便缓存队列中的数据对象。并没有实现读写分离,也就意味着生产和消费不能完全并行。是一个有界队列

LinkedBlockingQueue:基于列表的阻塞队列,在内部维护了一个数据缓冲队列(由一个链表构成),实现采用分离锁(读写分离两个锁),从而实现生产者和消费者操作的完全并行运行。是一个无界队列,也可以指定队列大小

SynchronousQueue: 没有缓存的队列,生存者生产的数据直接会被消费者获取并消费。若没有数据就直接调用出栈方法则会报错。

三种队列使用场景

newFixedThreadPool 线程池采用的队列是LinkedBlockingQueue。其优点是无界可缓存,内部实现读写分离,并发的处理能力高于ArrayBlockingQueue

newCachedThreadPool 线程池采用的队列是SynchronousQueue。其优点就是无缓存,接收到的任务均可直接处理,再次强调,慎用!

并发量不大,服务器性能较好,可以考虑使用SynchronousQueue。

并发量较大,服务器性能较好,可以考虑使用LinkedBlockingQueue。

并发量很大,服务器性能无法满足,可以考虑使用ArrayBlockingQueue。系统的稳定最重要。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
import org.junit.Test; /**
* 阻塞队列
* ArrayBlockingQueue :有界
* LinkedBlockingQueue :无界
* SynchronousQueue :无缓冲直接用
* 非阻塞队列
* ConcurrentLinkedQueue :高性能
*/
public class ITDragonQueue { /**
* ArrayBlockingQueue : 基于数组的阻塞队列实现,在内部维护了一个定长数组,以便缓存队列中的数据对象。
* 内部没有实现读写分离,生产和消费不能完全并行,
* 长度是需要定义的,
* 可以指定先进先出或者先进后出,
* 是一个有界队列。
*/
@Test
public void ITDragonArrayBlockingQueue() throws Exception {
ArrayBlockingQueue<String> array = new ArrayBlockingQueue<String>(5); // 可以尝试 队列长度由3改到5
array.offer("offer 插入数据方法---成功返回true 否则返回false");
array.offer("offer 3秒后插入数据方法", 3, TimeUnit.SECONDS);
array.put("put 插入数据方法---但超出队列长度则阻塞等待,没有返回值");
array.add("add 插入数据方法---但超出队列长度则提示 java.lang.IllegalStateException"); // java.lang.IllegalStateException: Queue full
System.out.println(array);
System.out.println(array.take() + " \t还剩元素 : " + array); // 从头部取出元素,并从队列里删除,若队列为null则一直等待
System.out.println(array.poll() + " \t还剩元素 : " + array); // 从头部取出元素,并从队列里删除,执行poll 后 元素减少一个
System.out.println(array.peek() + " \t还剩元素 : " + array); // 从头部取出元素,执行peek 不移除元素
} /**
* LinkedBlockingQueue:基于列表的阻塞队列,在内部维护了一个数据缓冲队列(该队列由一个链表构成)。
* 其内部实现采用读写分离锁,能高效的处理并发数据,生产者和消费者操作的完全并行运行
* 可以不指定长度,
* 是一个无界队列。
*/
@Test
public void ITDragonLinkedBlockingQueue() throws Exception {
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<String>();
queue.offer("1.无界队列");
queue.add("2.语法和ArrayBlockingQueue差不多");
queue.put("3.实现采用读写分离");
List<String> list = new ArrayList<String>();
System.out.println("返回截取的长度 : " + queue.drainTo(list, 2));
System.out.println("list : " + list);
} /**
* SynchronousQueue:没有缓冲的队列,生存者生产的数据直接会被消费者获取并消费。
*/
@Test
public void ITDragonSynchronousQueue() throws Exception {
final SynchronousQueue<String> queue = new SynchronousQueue<String>();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("take , 在没有取到值之前一直处理阻塞 : " + queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread1.start();
Thread.sleep(2000);
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
queue.add("进值!!!");
}
});
thread2.start();
} /**
* ConcurrentLinkedQueue:是一个适合高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,性能好于BlockingQueue。
* 它是一个基于链接节点的无界限线程安全队列。该队列的元素遵循先进先出的原则。头是最先加入的,尾是最后加入的,不允许null元素。
* 无阻塞队列,没有 put 和 take 方法
*/
@Test
public void ITDragonConcurrentLinkedQueue() throws Exception {
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<String>();
queue.offer("1.高性能无阻塞");
queue.add("2.无界队列");
System.out.println(queue);
System.out.println(queue.poll() + " \t : " + queue); // 从头部取出元素,并从队列里删除,执行poll 后 元素减少一个
System.out.println(queue.peek() + " \t : " + queue); // 从头部取出元素,执行peek 不移除元素
} }

关键字

关键字是为了线程安全服务的,哪什么是线程安全呢?当多个线程访问某一个类(对象或方法)时,这个对象始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的

线程安全的两个特性:原子性可见性。synchronized 同步,原子性。volatile 可见性。wait,notify 负责多个线程之间的通信。

synchronized

synchronized 可以在任意对象及方法上加锁,而加锁的这段代码称为"互斥区"或"临界区",若一个线程想要执行synchronized修饰的代码块,首先要

step1 尝试获得锁

step2 如果拿到锁,执行synchronized代码体内容

step3 如果拿不到锁,这个线程就会不断的尝试获得这把锁,直到拿到为止,而且是多个线程同时去竞争这把锁。

注*(线程多了也就是会出现锁竞争的问题,多个线程执行的顺序是按照CPU分配的先后顺序而定的,而并非代码执行的先后顺序)

synchronized 可以修饰方法,修饰代码块,这些都是对象锁。若和static一起使用,则升级为类锁。

synchronized 锁是可以重入的,当一个线程得到了一个对象的锁后,再次请求此对象时是可以再次得到该对象的锁。锁重入的机制,也支持在父子类继承的场景。

synchronized 同步异步,一个线程得到了一个对象的锁后,其他线程是可以执行非加锁的方法(异步)。但是不能执行其他加锁的方法(同步)。

synchronized 锁异常,当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

/**
* synchronized 关键字,可以修饰方法,也可以修饰代码块。建议采用后者,通过减小锁的粒度,以提高系统性能。
* synchronized 关键字,如果以字符串作为锁,请注意String常量池的缓存功能和字符串改变后锁是否的情况。
* synchronized 锁重入,当一个线程得到了一个对象的锁后,再次请求此对象时是可以再次得到该对象的锁。
* synchronized 同异步,一个线程获得锁后,另外一个线程可以执行非synchronized修饰的方法,这是异步。若另外一个线程执行任何synchronized修饰的方法则需要等待,这是同步
* synchronized 类锁,用static + synchronized 修饰则表示对整个类进行加锁
*/
public class ITDragonSynchronized { private void thisLock () { // 对象锁
synchronized (this) {
System.out.println("this 对象锁!");
}
} private void classLock () { // 类锁
synchronized (ITDragonSynchronized.class) {
System.out.println("class 类锁!");
}
} private Object lock = new Object();
private void objectLock () { // 任何对象锁
synchronized (lock) {
System.out.println("object 任何对象锁!");
}
} private void stringLock () { // 字符串锁,注意String常量池的缓存功能
synchronized ("string") { // 用 new String("string") t4 和 t5 同时进入。用string t4完成后,t5在开始
try {
for(int i = 0; i < 3; i++) {
System.out.println("thread : " + Thread.currentThread().getName() + " stringLock !");
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} private String strLock = "lock"; // 字符串锁改变
private void changeStrLock () {
synchronized (strLock) {
try {
System.out.println("thread : " + Thread.currentThread().getName() + " changeLock start !");
strLock = "changeLock";
Thread.sleep(500);
System.out.println("thread : " + Thread.currentThread().getName() + " changeLock end !");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} private synchronized void method1() { // 锁重入
System.out.println("^^^^^^^^^^^^^^^^^^^^ method1");
method2();
}
private synchronized void method2() {
System.out.println("-------------------- method2");
method3();
}
private synchronized void method3() {
System.out.println("******************** method3");
} private synchronized void syncMethod() {
try {
System.out.println(Thread.currentThread().getName() + " synchronized method!");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} // 若次方法也加上了synchronized,就必须等待t1线程执行完后,t2才能调用,两个synchronized块之间具有互斥性,synchronized块获得的是一个对象锁,锁定的是整个对象
private void asyncMethod() {
System.out.println(Thread.currentThread().getName() + " asynchronized method!");
} // static + synchronized 修饰则表示类锁,打印的结果是thread1线程先执行完,然后在执行thread2线程。若没有被static修饰,则thread1和 thread2几乎同时执行,同时结束
private synchronized void classLock(String args) {
System.out.println(args + "start......");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(args + "end......");
} public static void main(String[] args) throws Exception {
final ITDragonSynchronized itDragonSynchronized = new ITDragonSynchronized();
System.out.println("------------------------- synchronized 代码块加锁 -------------------------");
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.thisLock();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.classLock();
}
});
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.objectLock();
}
});
thread1.start();
thread2.start();
thread3.start();
Thread.sleep(2000);
System.out.println("------------------------- synchronized 字符串加锁 -------------------------");
// 如果字符串锁,用new String("string") t4,t5线程是可以获取锁的,如果直接使用"string" ,若锁不释放,t5线程一直处理等待中
Thread thread4 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.stringLock();
}
}, "t4");
Thread thread5 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.stringLock();
}
}, "t5");
thread4.start();
thread5.start(); Thread.sleep(3000);
System.out.println("------------------------- synchronized 字符串变锁 -------------------------");
// 字符串变了,锁也会改变,导致t7线程在t6线程未结束后变开始执行,但一个对象的属性变了,不影响这个对象的锁。
Thread thread6 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.changeStrLock();
}
}, "t6");
Thread thread7 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.changeStrLock();
}
}, "t7");
thread6.start();
thread7.start(); Thread.sleep(2000);
System.out.println("------------------------- synchronized 锁重入 -------------------------");
Thread thread8 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.method1();
}
}, "t8");
thread8.start();
Thread thread9 = new Thread(new Runnable() {
@Override
public void run() {
SunClass sunClass = new SunClass();
sunClass.sunMethod();
}
}, "t9");
thread9.start(); Thread.sleep(2000);
System.out.println("------------------------- synchronized 同步异步 -------------------------");
Thread thread10 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.syncMethod();
}
}, "t10");
Thread thread11 = new Thread(new Runnable() {
@Override
public void run() {
itDragonSynchronized.asyncMethod();
}
}, "t11");
thread10.start();
thread11.start(); Thread.sleep(2000);
System.out.println("------------------------- synchronized 同步异步 -------------------------");
ITDragonSynchronized classLock1 = new ITDragonSynchronized();
ITDragonSynchronized classLock2 = new ITDragonSynchronized();
Thread thread12 = new Thread(new Runnable() {
@Override
public void run() {
classLock1.classLock("classLock1");
}
});
thread12.start();
Thread thread13 = new Thread(new Runnable() {
@Override
public void run() {
classLock2.classLock("classLock2");
}
});
thread13.start();
} // 有父子继承关系的类,如果都使用了synchronized 关键字,也是线程安全的。
static class FatherClass {
public synchronized void fatherMethod(){
System.out.println("#################### fatherMethod");
}
} static class SunClass extends FatherClass{
public synchronized void sunMethod() {
System.out.println("@@@@@@@@@@@@@@@@@@@@ sunMethod");
this.fatherMethod();
}
}
}

volatile

volatile 关键字虽然不具备synchronized关键字的原子性(同步)但其主要作用就是使变量在多个线程中可见。也就是可见性。

用法很简单,直接用来修饰变量。因为其不具备原子性,可以用Atomic类代替。美中不足的是多个Atomic类也不具备原子性,所以还需要synchronized来修饰。

volatile 关键字工作原理

每个线程都有自己的工作内存,如果线程需要用到一个变量的时,会从主内存拷贝一份到自己的工作内存中。从而提高了效率。每次执行完线程后再将变量从工作内存同步回主内存中。

这样就存在一个问题,变量在不同线程中可能存在不同的值。如果用volatile 关键字修饰变量,则会让线程的执行引擎直接从主内存中获取值。

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; /**
* volatile 关键字主要作用就是使变量在多个线程中可见。
* volatile 关键字不具备原子性,但Atomic类是具备原子性和可见性。
* 美中不足的是多个Atomic类不具备原子性,还是需要synchronized 关键字帮忙。
*/
public class ITDragonVolatile{ private volatile boolean flag = true;
private static volatile int count;
private static AtomicInteger atomicCount = new AtomicInteger(0); // 加 static 是为了避免每次实例化对象时初始值为零 // 测试volatile 关键字的可见性
private void volatileMethod() {
System.out.println("thread start !");
while (flag) { // 如果flag为true则一直处于阻塞中,
}
System.out.println("thread end !");
} // 验证volatile 关键字不具备原子性
private int volatileCountMethod() {
for (int i = 0; i < 10; i++) {
// 第一个线程还未将count加到10的时候,就可能被另一个线程开始修改。可能会导致最后一次打印的值不是1000
count++ ;
}
return count;
} // 验证Atomic类具有原子性
private int atomicCountMethod() {
for (int i = 0; i < 10; i++) {
atomicCount.incrementAndGet();
}
// 若最后一次打印为1000则表示具备原子性,中间打印的信息可能是受println延迟影响。
return atomicCount.get();// 若最后一次打印为1000则表示具备原子性
} // 验证多个 Atomic类操作不具备原子性,加synchronized关键字修饰即可
private synchronized int multiAtomicMethod(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicCount.addAndGet(1);
atomicCount.addAndGet(2);
atomicCount.addAndGet(3);
atomicCount.addAndGet(4);
return atomicCount.get(); //若具备原子性,则返回的结果一定都是10的倍数,需多次运行才能看到结果
} /**
* volatile 关键字可见性原因
* 这里有两个线程 :一个是main的主线程,一个是thread的子线程
* jdk线程工作流程 :为了提高效率,每个线程都有一个工作内存,将主内存的变量拷贝一份到工作内存中。线程的执行引擎就直接从工作内存中获取变量。
* So 问题来了 :thread线程用的是自己的工作内存,主线程将变量修改后,thread线程不知道。这就是数据不可见的问题。
* 解决方法 :变量用volatile 关键字修饰后,线程的执行引擎就直接从主内存中获取变量。
*
*/
public static void main(String[] args) throws InterruptedException {
// 测试volatile 关键字的可见性
/*ITDragonVolatile itDragonVolatile = new ITDragonVolatile();
Thread thread = new Thread(itDragonVolatile);
thread.start();
Thread.sleep(1000); // 等线程启动了,再设置值
itDragonVolatile.setFlag(false);
System.out.println("flag : " + itDragonVolatile.isFlag());*/ // 验证volatile 关键字不具备原子性 和 Atomic类具有原子性
final ITDragonVolatile itDragonVolatile = new ITDragonVolatile();
List<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < 100; i++) {
threads.add(new Thread(new Runnable() {
@Override
public void run() {
// 中间打印的信息可能是受println延迟影响,请看最后一次打印的结果
System.out.println(itDragonVolatile.multiAtomicMethod());
}
}));
}
for(Thread thread : threads){
thread.start();
}
} public boolean isFlag() {
return flag;
} public void setFlag(boolean flag) {
this.flag = flag;
} }

wait,notify

使用 wait/ notify 方法实现线程间的通信,模拟BlockingQueue队列。有两点需要注意:

1)wait 和 notify 必须要配合 synchronized 关键字使用

2)wait方法是释放锁的, notify方法不释放锁。

线程通信概念:线程是操作系统中独立的个体,但这些个体如果不经过特殊的处理,就不能成为一个整体,线程之间的通信就成为整体的必用方法之一。

import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger; /**
* synchronized 可以在任意对象及方法上加锁,而加锁的这段代码称为"互斥区"或"临界区",一般给代码块加锁,通过减小锁的粒度从而提高性能。
* Atomic* 是为了弥补volatile关键字不具备原子性的问题。虽然一个Atomic*对象是具备原子性的,但不能确保多个Atomic*对象也具备原子性。
* volatile 关键字不具备synchronized关键字的原子性其主要作用就是使变量在多个线程中可见。
* wait / notify
* wait() 使线程阻塞运行,notify() 随机唤醒等待队列中等待同一共享资源的一个线程继续运行,notifyAll() 唤醒所有等待队列中等待同一共享资源的线程继续运行。
* 1)wait 和 notify 必须要配合 synchronized 关键字使用
* 2)wait方法是释放锁的, notify方法不释放锁
*/
public class ITDragonMyQueue { //1 需要一个承装元素的集合
private LinkedList<Object> list = new LinkedList<Object>();
//2 需要一个计数器 AtomicInteger (保证原子性和可见性)
private AtomicInteger count = new AtomicInteger(0);
//3 需要制定上限和下限
private final Integer minSize = 0;
private final Integer maxSize ; //4 构造方法
public ITDragonMyQueue(Integer size){
this.maxSize = size;
} //5 初始化一个对象 用于加锁
private final Object lock = new Object(); //put(anObject): 把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断,直到BlockingQueue里面有空间再继续.
public void put(Object obj){
synchronized (lock) {
while(count.get() == this.maxSize){
try {
lock.wait(); // 当Queue没有空间时,线程被阻塞 ,这里为了区分,命名为wait1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(obj); //1 加入元素
count.incrementAndGet(); //2 计数器累加
lock.notify(); //3 新增元素后,通知另外一个线程wait2,队列多了一个元素,可以做移除操作了。
System.out.println("新加入的元素为: " + obj);
}
} //take: 取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入.
public Object take(){
Object ret = null;
synchronized (lock) {
while(count.get() == this.minSize){
try {
lock.wait(); // 当Queue没有值时,线程被阻塞 ,这里为了区分,命名为wait2
} catch (InterruptedException e) {
e.printStackTrace();
}
}
ret = list.removeFirst(); //1 做移除元素操作
count.decrementAndGet(); //2 计数器递减
lock.notify(); //3 移除元素后,唤醒另外一个线程wait1,队列少元素了,可以再添加操作了
}
return ret;
} public int getSize(){
return this.count.get();
} public static void main(String[] args) throws Exception{
final ITDragonMyQueue queue = new ITDragonMyQueue(5);
queue.put("a");
queue.put("b");
queue.put("c");
queue.put("d");
queue.put("e");
System.out.println("当前容器的长度: " + queue.getSize());
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
queue.put("f");
queue.put("g");
}
},"thread1");
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("移除的元素为:" + queue.take()); // 移除一个元素后再进一个,而并非同时移除两个,进入两个元素。
System.out.println("移除的元素为:" + queue.take());
}
},"thread2");
thread1.start();
Thread.sleep(2000);
thread2.start();
}
}

死锁

死锁是一个很糟糕的情况,锁迟迟不能解开,其他线程只能一直处于等待阻塞状态。比如线程A拥有锁一,却还想要锁二。线程B拥有锁二,却还想要锁一。两个线程互不相让,两个线程将永远等待。

排查:

第一步:控制台输入jps用于获得当前JVM进程的pid

第二步:jstack pid 用于打印堆栈信息

第三步:解读,"Thread-1" 是线程的名字,prio 是线程的优先级,tid 是线程id, nid 是本地线程id, waiting to lock 等待去获取的锁,locked 自己拥有的锁。

"Thread-1" #11 prio=5 os_prio=0 tid=0x0000000055ff1800 nid=0x1bd4 waiting for monitor entry [0x0000000056e2e000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.itdragon.keyword.ITDragonDeadLock.rightLeft(ITDragonDeadLock.java:37)
- waiting to lock <0x00000000ecfdf9d0> (a java.lang.Object)
- locked <0x00000000ecfdf9e0> (a java.lang.Object)
at com.itdragon.keyword.ITDragonDeadLock$2.run(ITDragonDeadLock.java:54)
at java.lang.Thread.run(Thread.java:748)
/**
* 死锁: 线程A拥有锁一,却还想要锁二。线程B拥有锁二,却还想要锁一。两个线程互不相让,两个线程将永远等待。
* 避免: 在设计阶段,了解锁的先后顺序,减少锁的交互数量。
* 排查:
* 第一步:控制台输入 jps 用于获得当前JVM进程的pid
* 第二步:jstack pid 用于打印堆栈信息
* "Thread-1" #11 prio=5 os_prio=0 tid=0x0000000055ff1800 nid=0x1bd4 waiting for monitor entry [0x0000000056e2e000]
* - waiting to lock <0x00000000ecfdf9d0> - locked <0x00000000ecfdf9e0>
* "Thread-0" #10 prio=5 os_prio=0 tid=0x0000000055ff0800 nid=0x1b14 waiting for monitor entry [0x0000000056c7f000]
* - waiting to lock <0x00000000ecfdf9e0> - locked <0x00000000ecfdf9d0>
* 可以看出,两个线程持有的锁都是对方想要得到的锁(得不到的永远在骚动),而且最后一行也打印了 Found 1 deadlock.
*/
public class ITDragonDeadLock { private final Object left = new Object();
private final Object right = new Object(); public void leftRight(){
synchronized (left) {
try {
Thread.sleep(2000); // 模拟持有锁的过程
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (right) {
System.out.println("leftRight end!");
}
}
} public void rightLeft(){
synchronized (right) {
try {
Thread.sleep(2000); // 模拟持有锁的过程
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (left) {
System.out.println("rightLeft end!");
}
}
} public static void main(String[] args) {
ITDragonDeadLock itDragonDeadLock = new ITDragonDeadLock();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
itDragonDeadLock.leftRight();
}
});
thread1.start();
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
itDragonDeadLock.rightLeft();
}
});
thread2.start();
} }

多线程案例

若有Thread1、Thread2、Thread3、Thread4四条线程分别统计C、D、E、F四个盘的大小,所有线程都统计完毕交给Thread5线程去做汇总,应当如何实现

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; /**
* 若有Thread1、Thread2、Thread3、Thread4四条线程分别统计C、D、E、F四个盘的大小,所有线程都统计完毕交给Thread5线程去做汇总,应当如何实现?
* 思考:汇总,说明要把四个线程的结果返回给第五个线程,若要线程有返回值,推荐使用callable。Thread和Runnable都没返回值
*/
public class ITDragonThreads { public static void main(String[] args) throws Exception {
// 无缓冲无界线程池
ExecutorService executor = Executors.newFixedThreadPool(8);
// 相对ExecutorService,CompletionService可以更精确和简便地完成异步任务的执行
CompletionService<Long> completion = new ExecutorCompletionService<Long>(executor);
CountWorker countWorker = null;
for (int i = 0; i < 4; i++) { // 四个线程负责统计
countWorker = new CountWorker(i+1);
completion.submit(countWorker);
}
// 关闭线程池
executor.shutdown();
// 主线程相当于第五个线程,用于汇总数据
long total = 0;
for (int i = 0; i < 4; i++) {
total += completion.take().get();
}
System.out.println(total / 1024 / 1024 / 1024 +"G");
}
} class CountWorker implements Callable<Long>{
private Integer type;
public CountWorker() {
}
public CountWorker(Integer type) {
this.type = type;
} @Override
public Long call() throws Exception {
ArrayList<String> paths = new ArrayList<>(Arrays.asList("c:", "d:", "e:", "f:"));
return countDiskSpace(paths.get(type - 1));
} // 统计磁盘大小
private Long countDiskSpace (String path) {
File file = new File(path);
long totalSpace = file.getTotalSpace();
System.out.println(path + " 总空间大小 : " + totalSpace / 1024 / 1024 / 1024 + "G");
return totalSpace;
}
}

查考面试题

1 常见创建线程的方式和其优缺点

(1)继承Thread类 (2)实现Runnable接口

优缺点:实现一个接口比继承一个类要灵活,减少程序之间的耦合度。缺点就是代码多了一点。

2 start()方法和run()方法的区别

start方法可以启动线程,而run方法只是thread的一个普通方法调用。

3 多线程的作用

(1)发挥多核CPU的优势,提高CPU的利用率(2)防止阻塞,提高效率

4 什么是线程安全

当多个线程访问某一个类(对象或方法)时,这个对象始终都能表现出正确的行为,那么这个类(对象或方法)就是线程安全的。

5 线程安全级别

(1)不可变(2)绝对线程安全(3)相对线程安全(4)线程非安全

6 如何在两个线程之间共享数据

线程之间数据共享,其实可以理解为线程之间的通信,可以用wait/notify/notifyAll 进行等待和唤醒。

7 用线程池的好处

避免频繁地创建和销毁线程,达到线程对象的重用,提高性能,减轻服务器压力。使用线程池还可以根据项目灵活地控制并发的数目。

8 sleep方法和wait方法有什么区别

sleep方法和wait方法都可以用来放弃CPU一定的时间,sleep是thread的方法,不会释放锁。wait是object的方法,会释放锁。

总结

1 线程池核心参数有 初始核心线程数,线程池运行最大线程数,空闲线程存活时间,时间单位,任务队列。

2 队列是一种数据结构,主要有两类 阻塞队列BlockingQueue,和非阻塞高性能队列ConcurrentLinkedQueue。

3 线程安全的两个特性,原子性和可见性。synchronized 关键字具备原子性。volatile 关键字具备可见性。

4 单个Atomic类具备原子性和可见性,多个Atomic类不具备原子性,需要synchronized 关键字修饰。

5 两个线程持有的锁都是对方想要得到的锁时容易出现死锁的情况,从设计上尽量减少锁的交互。

本章到这里就结束了,涉及的知识点比较多,请参考流程图来学习。如有什么问题可以指出。喜欢的朋友可以点个"推荐"

线程池 队列 synchronized的更多相关文章

  1. 从线程池到synchronized关键字详解

    线程池 BlockingQueue synchronized volatile 前段时间看了一篇关于"一名3年工作经验的程序员应该具备的技能"文章,倍受打击.很多熟悉而又陌生的知识 ...

  2. javaWeb 使用线程池+队列解决"订单并发"问题

    解决方式:使用线程池+队列 项目基于Spring,如果不用spring需要自己把 ThreadPoolManager.java 改成单例模式 1.写一个Controller(Spring mvc) / ...

  3. Java线程池队列吃的太饱,撑着了咋整?java 队列过大导致内存溢出

    Java的Executors框架提供的定长线程池内部默认使用LinkedBlockingQueue作为任务的容器,这个队列是没有限定大小的,可以无限向里面submit任务. 当线程池处理的太慢的时候, ...

  4. java线程池之synchronized锁

    //Object 定义了一个引用类型的对象用于加锁 static Object Lock = new Object(); //定义一个int类型变量0做初始值 static int iCheck = ...

  5. 踩坑 Spring Cloud Hystrix 线程池队列配置

    背景: 有一次在生产环境,突然出现了很多笔还款单被挂起,后来排查原因,发现是内部系统调用时出现了Hystrix调用异常.在开发过程中,因为核心线程数设置的比较大,没有出现这种异常.放到了测试环境,偶尔 ...

  6. 源码剖析ThreadPoolExecutor线程池及阻塞队列

    本文章对ThreadPoolExecutor线程池的底层源码进行分析,线程池如何起到了线程复用.又是如何进行维护我们的线程任务的呢?我们直接进入正题: 首先我们看一下ThreadPoolExecuto ...

  7. Java 线程池的原理与实现

    最近在学习线程池.内存控制等关于提高程序运行性能方面的编程技术,在网上看到有一哥们写得不错,故和大家一起分享. 建议:在阅读本文前,先理一理同步的知识,特别是syncronized同步关键字的用法.关 ...

  8. Java 线程池的原理与实现(转)

    这几天主要是狂看源程序,在弥补了一些以前知识空白的同时,也学会了不少新的知识(比如 NIO),或者称为新技术吧.线程池就是其中之一,一提到线程,我们会想到以前<操作系统>的生产者与消费者, ...

  9. 创建Java线程池

    线程池的作用: 线程池作用就是限制系统中执行线程的数量. 根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果:少了浪费了系统资源,多了造成系统拥挤效率不高.用线程池控制线程数量,其他线 ...

随机推荐

  1. 合并查询结果集UNION(去重), UNION ALL(不去重),INTERSECT(交集),MINUS(差集,第一个结果集减去第二个结果集,第一个结果集中不在第二个结果集中的记录行),[NOT] EXIST

    MINUS配合[NOT] EXIST使用可以查询出包含符合某个条件的多记录的其他记录, 举例: 顾客A买了商品2.4.6 顾客B买了商品1.2.4 顾客C买了商品4.6 顾客D买了商品1.2.4.6 ...

  2. java显示目录文件列表和删除目录

    */ .hljs { display: block; overflow-x: auto; padding: 0.5em; color: #333; background: #f8f8f8; } .hl ...

  3. JavaScript基础4——关于语句流程控制(分支语句、循环语句等)

    分支语句 (1)if...else...语句,基本格式分三种,如下 <script type="text/javascript"> var i=50; //if语句 i ...

  4. Charles 抓包工具使用部分问题总结

    一. You may need to configure your browser or application to trust the Charles Root Certificate. See ...

  5. NOIP2017day1游记

    NOIP 2017总结 Day1 Day1T1 第一眼看到瞬间慌掉,woc这玩意啥! 然后懵逼了两分钟 好的 我相信他是NOIP第一题 那我就打个表吧 然后花五分钟打了个暴力 玩了几组数据 哇!好像有 ...

  6. Java中 &&与&,||与|的区别

    区别 &&  || 是逻辑运算,支持短路运算 & | 是位运算,不支持短路运算 短路运算 当有多个表达式时,左边的表达式值可以确定结果时,就再继续运算右边的表达式的值; 举例 ...

  7. bzoj 2565: 最长双回文串

    Description 顺序和逆序读起来完全一样的串叫做回文串.比如acbca是回文串,而abc不是(abc的顺序为"abc",逆序为"cba",不相同).输入 ...

  8. Regular expressions in lexing and parsing(翻译)

    词法分析和语法分析中的正则表达式 (英文原文来自rob pike 的博客 https://commandcenter.blogspot.jp/2011/08/regular-expressions-i ...

  9. shiro Filter--拦截器

    一 shiro自带的filter:下面主要叙述顺序是 NameableFilter->OncePerRequestFilter->AdviceFilter->PathMatching ...

  10. php SeasLog使用以及liunx环境下安装

    1.下载SeasLog http://pecl.php.net/package/SeasLog php官方 https://github.com/Neeke/SeasLog 作者的github  2. ...