学习路径:基础入门-》初步应用-》高级-》源码分析
并发编程学习目录:
1,线程基础、线程之间的共享和协作
2,线程的并发工具类
3,原子操作CAS
4,显示锁和AQS
5,并发容器
6,线程池和Exector框架
7,线程安全
8a,实战项目-并发任务执行框架
8b,实战项目-性能优化实战
9,JMM和底层原理
10,java8新增的并发
*java并发编程-线程基础-线程之间的共享和协作
1,基础概念:
什么是进程和线程:
CPU核心数和线程数的关系:
CPU时间片轮转机制:
并行和并发:
并行,可以同时进行的任务数
并发,应用可以交替的执行任务
高并发编程的意义、好处和注意事项:
充分利用CPU资源,
2,认识 java 里的线程
2.1 线程的创建:
JDK的Thread类中,注释说明了创建线程的方式是两种: There are two ways to create a new thread of execution
1,类 Thread
2,接口 Runnable
Thread 和 Runnable 的区别:
- java对线程的抽象:Thread
- 对任务的抽象:Runnable
2.2 启动线程
start() 方法与 run() 方法的区别:
start() 方法:
-
通过调用 start() 方法启动线程
-
如果调用两次 start() 方法,第二次调用时会报错:Exception in thread “main” java.lang.IllegalThreadStateException,第二次start() 方法后面的代码不会执行,但是不会影响已经启动的子线程,因为子线程运行在新开辟的栈内。
Thread 类 start() 方法部分源码:
if (threadStatus != 0)
throw new IllegalThreadStateException();
示例:
ThreadRun threadRun = new ThreadRun();
threadRun.setName("threadRun");
threadRun.start();
System.out.println("第二次调用方法前。。");
threadRun.start(); // Exception in thread "main" java.lang.IllegalThreadStateException
System.out.println("第二次调用方法后。。");
控制台打印如下:
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at com.concurrent.ch1.base.StartAanRun.main(StartAanRun.java:33)
第二次调用方法前。。
i am threadRun and now the i=3
i am threadRun and now the i=2
i am threadRun and now the i=1
Disconnected from the target VM, address: '127.0.0.1:52301', transport: 'socket'
run() 方法:
- run() 方法是线程启动后将要执行的方法
- 如果创建线程后,直接调用 run() 方法,不会让线程进入“就绪”状态,而是普通的java方法调用,是代码是串行执行的,并没有新开辟一个栈空间。
2.3 线程的终止:
用 stop() 还是 interrupt()、isInterrupted() 、static 方法 interrupted() ?
JDK 里的线程是协作式的,而不是抢占式的。
interrupt() 方法
对当前线程发起中断,将中断标识位从false改成true,线程不会立即停止,线程可能会中断,可能也不会中断。
isInterrupted() 方法
判断中断标识位有没有被置成 true,当前线程需不需要中断。(工作中常用)
继承Thread类的线程中断,代码示例:
package com.concurrent.ch1.base.safeend;
import java.util.concurrent.TimeUnit;
/**
* @author h
* @Description TODO
* @createTime 2020年03月07日 23:44
*/
public class EndThread {
private static class UseThread extends Thread {
public UseThread(String name) {
super(name);
}
@Override
public void run() {
String threaedName = Thread.currentThread().getName();
System.out.println(threaedName + " interrupt flag = " + isInterrupted());
while (!isInterrupted()) {
System.out.println(threaedName + " is running");
System.out.println(threaedName + " inner interrupt flag = " + isInterrupted());
}
System.out.println(threaedName + " inner interrupt flag = " + isInterrupted());
}
}
public static void main(String[] args) {
UseThread endThread = new UseThread("endThread");
endThread.start(); // 线程启动后 interrupt 中断标识位默认为false
try { TimeUnit.MILLISECONDS.sleep(5);} catch (Exception e) {e.printStackTrace();}
endThread.interrupt();// 中断线程,其实是设置线程的标识位true
}
}
实现Runnable接口的线程中断,代码示例:
package com.concurrent.ch1.base.safeend;
import java.util.concurrent.TimeUnit;
/**
* @author h
* @Description TODO
* @createTime 2020年03月07日 23:54
*/
public class EndRunnable {
private static class UseRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()
+ " interrupt flag is " + Thread.currentThread().isInterrupted());
while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName()
+ " i am implements Runnable.");
}
System.out.println(Thread.currentThread().getName()
+ " interrupt flag is " + Thread.currentThread().isInterrupted());
}
}
public static void main(String[] args) {
UseRunnable useRunnable = new UseRunnable();
Thread endThread = new Thread(useRunnable, "endThread");
endThread.start();
try { TimeUnit.MILLISECONDS.sleep(10);} catch (Exception e) {e.printStackTrace();}
endThread.interrupt();
}
}
线程中使用了“阻塞类的方法”如:sleep();的线程中断,代码示例:
会抛出 InterruptedException 异常
package com.concurrent.ch1.base.safeend;
/**
* @author h
* @Description 阻塞方法中抛出InterruptedException异常后
* 如果要继续中断,需要手动再次中断一次
* @createTime 2020年03月09日 22:16
*/
public class HashInterrputException {
private static class UseThread extends Thread {
public UseThread(String name) {
super(name);
}
@Override
public void run() {
while (!isInterrupted()){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// 抛出 InterruptedException 异常后,会把isInterrupted赋值会false
System.out.println(Thread.currentThread().getName()
+ " in InterruptedException interrupt flag is "
+ isInterrupted());
// 释放资源
interrupt();
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ " I am extends Thread.");
}
System.out.println(Thread.currentThread().getName()
+ " in InterruptedException interrupt flag is "
+ isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
UseThread endThread = new UseThread("HashInterruptEx");
endThread.start();
Thread.sleep(500);
endThread.interrupt();
}
}
控制台打印结果:
虽然在主线程中调用了 endThread.interrupt();方法,但是在线程的run()方法中抛出InterruptedException 异常后,中断标识位 会被修改成 false,如果不这样做就和调用stop()没有区别了,所以需要真正的停止线程时,需要再次手动调用一次 interrupt(); 方法才能停止,否则线程不会中断。
HashInterruptEx I am extends Thread.
HashInterruptEx I am extends Thread.
HashInterruptEx in InterruptedException interrupt flag is false
HashInterruptEx I am extends Thread.
HashInterruptEx in InterruptedException interrupt flag is true
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.concurrent.ch1.base.safeend.HashInterrputException$UseThread.run(HashInterrputException.java:21)
如果把run() 方法中的 // 释放资源 // interrupt(); 注释掉(即抛出异常后不进行手动中断),控制台打印结果如下:
HashInterruptEx I am extends Thread.
HashInterruptEx I am extends Thread.
HashInterruptEx in InterruptedException interrupt flag is false
HashInterruptEx I am extends Thread.
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.concurrent.ch1.base.safeend.HashInterrputException$UseThread.run(HashInterrputException.java:21)
HashInterruptEx I am extends Thread.
HashInterruptEx I am extends Thread.
。。。。。。
。。。。。。
static 方法 interrupted()
同样也可以判断中断标识位是否被置成 true,如果被置成 true,该方法会把标识位重新改成 false。
中断线程的坑:
不要在线程类中设置一个类似boolean类型的标志位,当线程方法中使用了阻塞类方法时,可能线程会一直处于等待状态。所以我们需要中断线程应该使用 interrupt() 方法来中断,因为Thread类中的阻塞类方法中如 sleep方法 public static native void sleep(long millis) throws InterruptedException;
package com.concurrent.ch1.base.safeend;
import java.util.concurrent.TimeUnit;
public class BadEndThread {
public static class UseThread extends Thread {
private boolean cancel = true;
@Override
public void run() {
while (cancel) {
System.out.println("开始。。。");
// 当线程中调用了阻塞类的方法时,当前线程被当前系统挂起了,此时不会去检测cancel标识位
// 还是会在这里等待,直到睡眠时间结束。
try { Thread.sleep(5000);} catch (Exception e) {e.printStackTrace();}
// wait(); // wait方法会一直处于等待状态
// take(); // 要满足take方法的条件才可以
System.out.println("线程执行。。。");
}
}
public boolean getCancel() {
return cancel;
}
public void setCancel(boolean cancel) {
this.cancel = cancel;
}
}
public static void main(String[] args) {
UseThread useThread = new UseThread();
useThread.start();
try { TimeUnit.SECONDS.sleep(1);} catch (Exception e) {e.printStackTrace();}
System.out.println("修改标识位。");
useThread.setCancel(false);
System.out.println("主线程执行完毕。。。");
}
}
控制台打印的信息:
开始。。。
修改标识位。
主线程执行完毕。。。
线程执行。。。
3 对java里的线程再多一点认识
3.1 线程状态
3.2 线程常用方法
深入理解 run() 和 start()
- 直接调用 run() 时是普通方法的调用,而不是启动一个新的线程。
- 调用 start() 方法会启动一个新的线程,开辟一个栈空间用来执行线程。
yield():了解
- 让出CPU时间片,当前线程重新进入就绪状态,一起参与下一轮抢夺CPU时间片,有可能会抢到也可能会抢不到。
- 注意执行 yield() 方法,不会释放锁资源,只是让出当前CPU时间片。
join() 方法
- 在一个线程A中,有另外的一个线程B调用了 join() 方法后,线程A会被挂起,直到线程B的run()方法执行完之后,才会执行会线程A的run()方法。
面试考点:
怎样让两个线程按照顺序执行,即必须要一个线程先执行完,另一个线程才能执行(使用 join() 方法怎样实现)
代码示例:
package com.concurrent.ch1.base;
import java.util.concurrent.TimeUnit;
/**
* @author h
* @Description 演示join() 方法的使用
* @createTime 2020年03月09日 23:17
*/
public class UseJoin {
static class Goddess implements Runnable {
private Thread thread;
// 这里用构造方法传入一个线程,是为了让这个线程可以在Goddess线程的run()方法里执行join()方法
// 这样才能才能挂起Goddess线程
public Goddess(Thread thread) {
this.thread = thread;
}
public Goddess() {
}
@Override
public void run() {
System.out.println("Goddess 开始排队打饭。。");
try {
if (thread != null) {
thread.join();
}
} catch (Exception e) {
e.printStackTrace();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ " Goddess打饭完成。");
}
}
static class GoddessBoyfriend implements Runnable {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("GoddessBoyfriend开始排队打饭。。");
System.out.println(Thread.currentThread().getName()
+ " GoddessBoyfriend打饭完成。。");
}
}
public static void main(String[] args) throws InterruptedException {
Thread goddessBoyfriend = new Thread(new GoddessBoyfriend());
Thread goddess = new Thread(new Goddess(goddessBoyfriend));
// Thread goddess = new Thread(new Goddess());
goddess.start();
goddessBoyfriend.start();
System.out.println("主线程 开始排队打饭。。");
goddess.join();
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 主线程打饭完成。");
}
}
控制台打印结果:
主线程 开始排队打饭。。
Goddess 开始排队打饭。。
GoddessBoyfriend开始排队打饭。。
Thread-0 GoddessBoyfriend打饭完成。。
Thread-1 Goddess打饭完成。
main 主线程打饭完成。
3.3 线程的优先级:
- 优先级分为 1-10,默认是 5,数字越大可能分配到的CPU时间片会多一些。
3.4 守护线程:
注意:
- 守护线程中finally不一定起作用,主要看用户线程分配到的CPU时间片是否够执行完 finally 语句块的时间。所以在守护线程中,不能在finally语句块来确保释放资源和连接关闭的操作。
- 用户线程全部结束后,守护线程才结束。
代码示例:
package com.concurrent.ch1.base;
/**
* @author h
* @Description 守护线程的使用
* @createTime 2020年03月09日 23:41
*/
public class DaemonThread {
private static class UseThread extends Thread {
@Override
public void run() {
try {
while(!isInterrupted()){
System.out.println(Thread.currentThread().getName()
+ " I am extends Thread");
}
System.out.println(Thread.currentThread().getName()
+ " interrupt flag is " + isInterrupted());
} finally {
// 守护线程中finally不一定起作用
System.out.println("..........finally");
}
}
}
public static void main(String[] args) throws InterruptedException {
UseThread useThread = new UseThread();
useThread.setDaemon(true);
useThread.start();
System.out.println("主线程。。。。");
Thread.sleep(5);
//useThread.interrupt();
}
}
控制台打印结果:
主线程。。。。
Thread-0 I am extends Thread
Thread-0 I am extends Thread
Thread-0 I am extends Thread
Thread-0 I am extends Thread
4 线程间的共享
什么是线程间的共享:
4.1 synchronized 内置锁
- 用处和用法:同步块、同步方法
- 对象锁:通常会另外Object obj = new Object(); 来作为对象锁,如果对象锁不是同一个对象,运行时会并行执行。
package com.concurrent.ch1.syn;
/**
* @author h
* @Description synchronized 关键字的使用方法
* SynTest:资源共享类
* @createTime 2020年03月10日 22:26
*/
public class SynTest {
private long count = 0;
// 作为一个锁
private Object obj = new Object();
public long getCount() {
return count;
}
public void setCount(long count) {
this.count = count;
}
/**用在同步块上,对象锁*/
public void incCount(){
synchronized(obj){
count++;
}
}
/**用在方法上,对象锁*/
public synchronized void incCount2(){
count++;
}
/**用在同步块上,但是锁的是当前类的对象实例*/
public void incCount3(){
synchronized(this){
count++;
}
}
// 线程
public static class Count extends Thread{
private SynTest simplOper;
public Count(SynTest simplOper){
this.simplOper = simplOper;
}
@Override
public void run(){
for (int i = 0; i < 10000; i++) {
simplOper.incCount2();
}
}
}
public static void main(String[] args) throws InterruptedException {
SynTest synTest = new SynTest();
Count count = new Count(synTest);
Count count2 = new Count(synTest);
count.start();
count2.start();
Thread.sleep(50);
System.out.println(synTest.count);
}
}
- 类锁:
示例:如果有两个线程分别调用 synClass() 方法和 synStatic() 方法时,是可以并行执行的,因为synClass()用的是类锁,synStatic()用的是 obj 的对象锁,两个线程使用的是两把不同的锁,所以两个线程会并行执行。
4.2 错误的加锁和原因分析
一下代码示例中,i++底层是使用了 Integer.valueOf()方法,每次都是new了一个新对象。导致synchronized锁的对象发生了变化
package com.concurrent.ch1.syn;
import java.util.concurrent.TimeUnit;
/**
* @author h
* @Description 错误的加锁和原因分析
* @createTime 2020年03月10日 23:39
*/
public class TestIntegerSyn {
public static class Worker implements Runnable {
private Integer i;
public Worker(Integer i) {
this.i = i;
}
@Override
public void run() {
synchronized (i) {
System.out.println(Thread.currentThread().getName()
+"--@"+System.identityHashCode(i));
i++;
System.out.println(Thread.currentThread().getName()
+"-----"+i+"--@"+System.identityHashCode(i));
try { TimeUnit.SECONDS.sleep(1);} catch (Exception e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName()
+"-----"+i+"--@"+System.identityHashCode(i));
}
}
}
public static void main(String[] args) {
Worker worker = new Worker(1);
for (int i = 0; i < 5; i++) {
new Thread(worker).start();
}
}
}
4.3 volatile 关键字,最轻量的同步机制(保证可见性,不保证原子性)
- 保证可见性
- 不能保证原子性
保证可见性代码示例如下:
package com.concurrent.vola;
import java.util.concurrent.TimeUnit;
/**
* @author h
* @Description 演示volatile的可见性
* @createTime 2020年03月10日 23:56
*/
public class VolatileCase {
private static boolean ready;
// private static volatile boolean ready;
private static int number;
private static class PrintThread extends Thread{
@Override
public void run(){
System.out.println("PrintThread is running...");
while(!ready){}; // 无线循环
System.out.println("number = " + number);
}
}
public static void main(String[] args) {
new PrintThread().start();
// 这里加休眠是为了防止子线程还没有真正启动number和ready就已经被主线程修改完了
try { TimeUnit.SECONDS.sleep(1);} catch (Exception e) {e.printStackTrace();}
number = 50;
ready = true;
try { TimeUnit.SECONDS.sleep(3);} catch (Exception e) {e.printStackTrace();}
System.out.println("main is ended!");
}
}
控制台打印结果:添加volatile关键字后,子线程才会打印number
PrintThread is running...
main is ended!
5 ThreadLocal 辨析
5.1 ThreadLocal 的使用
在spring的事务中使用到了 ThreadLocal 关键字,用来保存每个线程的连接。
每个线程都有变量的副本,线程的隔离
代码示例:
package com.concurrent.ch1.threadlocal;
/**
* @author h
* @Description 演示ThreadLocal的使用
* @createTime 2020年03月11日 22:15
*/
public class UseThreadLocal {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 1;
}
};
/**
* 运行3个线程
*/
public void startThreadArray(){
Thread[] threads = new Thread[3];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new TestThread(i));
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
}
public static class TestThread implements Runnable{
int id;
public TestThread(int id){
this.id = id;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" start");
Integer s = threadLocal.get();
s = s + id;
threadLocal.set(s);
System.out.println(Thread.currentThread().getName()
+ ": " + threadLocal.get());
// threadLocal.remove();
}
}
public static void main(String[] args) {
UseThreadLocal useThreadLocal = new UseThreadLocal();
useThreadLocal.startThreadArray();
}
}
控制台打印结果,如下:
Thread-0 start
Thread-2 start
Thread-2: 3
Thread-0: 1
Thread-1 start
Thread-1: 2
不使用ThreadLocal的代码示例:
package com.concurrent.ch1.threadlocal;
/**
* @author h
* @Description 演示未使用ThreadLocal的情况
* @createTime 2020年03月11日 22:43
*/
public class NoThreadLocal {
static Integer count = new Integer("1");
public static class TestThread implements Runnable{
int id;
public TestThread(int id){
this.id = id;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":start");
count = count + id;
System.out.println(Thread.currentThread().getName()
+":" + count);
}
}
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(new TestThread(i)).start();
}
}
}
控制台打印结果如下:
Thread-0:start
Thread-0:1
Thread-1:start
Thread-1:2
Thread-2:start
Thread-2:4
5.2 每个线程都有自己的变量副本,保证线程间的隔离
原理图:
每个线程都有各自的ThreadLocalMap,线程中定义的ThreadLocal作为key,各自线程的value值保存到各自线程的ThreadLocalMap中,达到线程隔离的效果,Entry[] table是为了让线程中可以定义多个ThreadLocal。
底层部分源码如下:
get() 方法
ThreadLocal.ThreadLocalMap threadLocals = null;
public T get() {
Thread t = Thread.currentThread();
// 获取当前线程中的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从当前线程的ThreadLocalMap中,获取变量副本的值,并返回
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
}
5.3 ThreadLocal引发的内存泄漏分析
内存泄漏代码示例:
package com.concurrent.ch1.threadlocal;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author h
* @Description ThreadLocal造成的内存泄漏演示
* @createTime 2020年03月11日 23:15
*/
public class ThreadLocalOOM {
private static final int TASK_LOOP_SIZE = 500;
final static ThreadPoolExecutor poolExecutor
= new ThreadPoolExecutor(5,
5,
1,
TimeUnit.MINUTES,
new LinkedBlockingQueue<>());
static class LocalVariable {
private byte[] a = new byte[1024*1024*5];//5M大小的数组
}
final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < TASK_LOOP_SIZE; i++) {
poolExecutor.execute(() ->{
localVariable.set(new LocalVariable());
// new LocalVariable();
System.out.println("use local variable");
// localVariable.remove();
});
Thread.sleep(100);
}
System.out.println("pool execute over");
}
}
使用 JDK bin目录中的 jvisualvm.exe可以看到堆使用情况如下:
示例代码中,运行了5个线程,每个线程创建了5M大小的数组,大概使用内存为 25M。而实际最大使用内存为 180M 左右,即使线程运行结束后也占用了 70多M 的内容,这就是内存泄漏的现象。
引发的内存泄漏分析
- 强引用
强引用示例: Object o = new Object();
引用一直存在,不会进行垃圾回收。 - 软引用(SoftReference)
将要发生内存溢出,进行一次垃圾回收,回收后发现内存还不够用时,就会不管栈上有没有指针指向软引用(SoftRefence),都会对软引用进行垃圾回收。 - 弱引用(WeakReference)
只要发生垃圾回收,一定会对弱引用指向的对象进行回收。
ThreadLocal中使用的就是弱引用, - 虚引用
发生GC时,通知
ThreadLocal.ThreadLocalMap 中的静态内部类使用的是弱引用,只要一发生垃圾回收就会被回收。
static class Entry extends WeakReference<ThreadLocal<?>> {}
原理图如下:
原理图说明:
ThreadLocalRef:当前线程中的ThreadLocal变量的引用
CurrentThreadRef:当前线程本身的引用
当ThreadLocalTef被置为 null 后,栈中就没有指针指向堆中的ThreadLocal了,此时只剩下堆中的key指向ThreadLocal,但是这个指向是弱引用,在发生垃圾回收时,堆中的ThreadLocal会被回收掉,key变成null,而当前线程的引用 CurrentThread 还持有Map,Map又还持有Entry及其value,但是Entry的key已经被回收了,没有办法再获取到value值,除非是当前线程本省 CurrentThread 被回收了,这个value才会被回收掉。
而上面的内存监控图中,内存都是在100M左右上下浮动,为什么内存没有直线上升呢?
原因是,ThreadLocal 的 set 方法中有清理这部分泄漏内存(key为 null 的Entry)的处理。但是这个回收是有延迟的。
源码如下:
get() 方法中的清除逻辑 getEntryAfterMiss
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
set() 方法中的清理逻辑
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
*解决ThreadLocal内存泄漏的措施:
- 在使用 set() 方法后要手动调用 remove() 方法。
5.4 ThreadLocal 的线程不安全
示例代码如下:
package com.concurrent.ch1.threadlocal;
import java.util.concurrent.TimeUnit;
/**
* @author h
* @Description ThreadLocal 的线程不安全演示
* @createTime 2020年03月12日 22:58
*/
public class ThreadLocalUnsafe implements Runnable {
public static Number number = new Number(0);
public static ThreadLocal<Number> value = new ThreadLocal<Number>(){};
// public Number number = new Number(0); // 解决方案一
// 解决方案二
// public static ThreadLocal<Number> value = new ThreadLocal<Number>(){
// @Override
// protected Number initialValue() {
// return new Number(0);
// }
// };
@Override
public void run() {
// 每个线程计数加一
number.setNum(number.getNum() + 1);
// Number number = value.get(); // 解决方案二
// number.setNum(number.getNum() + 1); // 解决方案二
// 将其存储到 ThreadLocal中
value.set(ThreadLocalUnsafe.number);
// value.set(number); // 解决方案二
try { TimeUnit.SECONDS.sleep(2);} catch (Exception e) {e.printStackTrace();}
// 输出num的值
System.out.println(Thread.currentThread().getName()+ " = " + value.get().getNum());
value.remove();
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(new ThreadLocalUnsafe()).start();
}
}
public static class Number{
private int num;
public Number(int num){
this.num = num;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}
}
控制台打印结果,如下:
Thread-3 = 5
Thread-1 = 5
Thread-2 = 5
Thread-0 = 5
Thread-4 = 5
线程不安全,原因分析:
变量 Number 被定义成了 static 的,即 5 个线程都共享同一个对象,虽然每个线程都有拷贝各自的ThreadLocal但是这些 ThreadLocal 中的 Number 都是指向同一个对象,并没有真正的实现线程隔离。
解决措施:
- 1,变量number 去掉 static的修饰,public Number number = new Number(0);
- 2,直接在定义 ThreadLocal 时初始化一下
public static ThreadLocal<Number> v = new ThreadLocal<Number>(){
@Override
protected Number initialValue() {
return new Number(0);
}
};
6 线程间协作
6.1 什么是线程间的协作:
线程间相互协作,完成同一件事。
经典例子:生产者,消费者
6.2 等待和通知:
wait()
一个线程调用了 wait() 方法之后,当前线程会释放持有的锁。
notify()/notifyAll()
调用 notify()/notifyAll() 不会做释放锁的动作,所以一般 notify()/notifyAll() 会放在同步代码块的最后一行。
等待和通知的标准范式
消费者(等待):
synchronized(对象){
while(条件不满足){
对象.wait();
}
// 业务逻辑
}
生产者(通知):
synchronized(对象){
// 业务逻辑,改变条件
// do something
对象.notify(); 或 对象.notifyAll();
}
注意,面试考点:
- wait()、notify()、notifyAll()一定要使用在 synchronized 关键字的代码包裹中。
- 当等待的线程持有锁但条件不满足时,线程会进入等待状态。而通知线程又在等待对象锁的释放。所以在等待线程调用 对象.wait(); 方法真正休眠之前会把持有的对象锁释放后才进入睡眠状态。
- 调用notify()、notifyAll() 方法,不会释放当前线程持有的锁,要等到锁包裹的代码全部执行完才会释放。所以一般会把 notify()、notifyAll() 放在代码的最后一行。
- 当前线程被“唤醒”后,才会参与竞争锁。
notify() 和 notifyAll() 应该用谁?
notify() 方法只能唤醒一个等待的线程,但是唤醒的线程不一定是想要唤醒的线程,所以应该尽量使用 notifyAll() 方法。
生产者,消费者代码示例:
实体类:
package com.concurrent.ch1.wn;
/**
* @author h
* @Description 快递实体类:
* 生产者和消费者的代码都是写在实体类中,
* 然后创建两个线程,这两个线程使用相同的实体类,线程的run()方法中调用实体类的消费方法
* 最后在主线程中创建线程,
* @createTime 2020年03月13日 21:52
*/
public class Express {
public final static String CITY = "Shanghai";
private int km; // 快递运输里程数
private String site; // 快递到达地点
public Express(){}
public Express(int km, String site){
this.km = km;
this.site = site;
}
public int getKm() {
return km;
}
public void setKm(int km) {
this.km = km;
}
public String getSite() {
return site;
}
public void setSite(String site) {
this.site = site;
}
// 生产者:变化公里数,然后通知处于wait状态并需要处理公里数的线程进行业务处理
public synchronized void changeKm(){
this.km = 101;
notifyAll();
}
// 变化地点,然后通知处于wait状态并需要处理地点的线程进行业务处理
public synchronized void changeSite(){
this.site = "BeiJing";
notifyAll();
}
public synchronized void waitKm(){
// 这里使用while循环,是因为不满足条件时代码会在wait();方法的这行代码这里等待,
// 当这个等待的线程被唤醒后,会继续执行wait();方法后的代码,但是为了确保此时条件一定是满足的,所以用while循环再次判断了一次
// 只有真正满足条件时,才能执行while循环后的代码。
while (this.km < 100){
// 条件不满足,进入等待状态
try {
System.out.println(Thread.currentThread().getName()+"the Km is "+this.km+", I will wait");
wait();
System.out.println(Thread.currentThread().getName()+"the Km is "+this.km+", I wait end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+" the Km is "+this.km+", I will change db");
}
// 消费者:
public synchronized void waitSite(){
while (CITY.equals(this.site)){ // 快递到达的目的地
try {
// 条件不满足进入等待状态
wait();
System.out.println("Check Site thread["+Thread.currentThread().getId()
+"] is be notified");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+" the site is "+this.site+",I will call user");
}
}
测试类:
package com.concurrent.ch1.wn;
import java.util.concurrent.TimeUnit;
/**
* @author h
* @Description 测试wait/notify/notifyAll
* @createTime 2020年03月13日 22:10
*/
public class TestWN {
private static Express express = new Express(0, Express.CITY);
// 检查里程数变化的线程,不满足条件,线程一直等待
private static class CheckKm extends Thread{
@Override
public void run(){
express.waitKm();
}
}
// 检查地点变化的线程,不满足条件,线程一直等待
private static class CheckSite extends Thread{
@Override
public void run(){
express.waitSite();
}
}
// 在主线程中创建线程,并改变条件
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new CheckSite().start();
}
for (int i = 0; i < 3; i++) {
new CheckKm().start();
}
try { TimeUnit.SECONDS.sleep(10);} catch (Exception e) {e.printStackTrace();}
express.changeKm();
}
}
生产者、消费者,实际面试题:
- 采用多线程技术,例如wait/notify,设计实现一个符合生产者和消费者问题的程序,
- 对某一个对象(枪膛)进行操作,其最大容量是20颗子弹,
- 生产者线程是一个压入线程,它不断向枪膛中压入子弹,消费者线程是一个射出线程,
- 它不断从枪膛中射出子弹。
- 请实现上面的程序。
代码示例如下:
package com.concurrent.ch1.wn;
/**
* @author h
* @Description 生产者和消费者
* 采用多线程技术,例如wait/notify,设计实现一个符合生产者和消费者问题的程序,
* 对某一个对象(枪膛)进行操作,其最大容量是20颗子弹,
* 生产者线程是一个压入线程,它不断向枪膛中压入子弹,消费者线程是一个射出线程,
* 它不断从枪膛中射出子弹。
* 请实现上面的程序。
* @createTime 2020年03月15日 11:20
*/
public class ProductAndConsumer {
public static void main(String[] args) {
ProductAndConsumer productAndConsumer = new ProductAndConsumer(20);
ProductThread productThread = new ProductThread(productAndConsumer);
ConsumerThread consumerThread = new ConsumerThread(productAndConsumer);
productThread.start();
consumerThread.start();
}
private int count = 0;
private int limitCount;
private static final Object OBJ = new Object();
public ProductAndConsumer(int limitCount) {
this.limitCount = limitCount;
}
public void product() {
synchronized (OBJ) {
while (count >= limitCount) {
System.out.println("弹夹已满,等待发射子弹。");
try {
OBJ.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(System.currentTimeMillis() + " 放入第 " + (++count) + " 颗子弹成功。");
OBJ.notifyAll();
}
}
public void consumer() {
synchronized (OBJ) {
while (count <= 0) {
try {
System.out.println("弹夹已空,等待装入子弹。");
OBJ.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(System.currentTimeMillis() + " 成功发射第 " + (count--) + " 颗子弹。");
OBJ.notifyAll();
}
}
private static class ProductThread extends Thread {
public ProductAndConsumer productAndConsumer;
public ProductThread(ProductAndConsumer productAndConsumer) {
this.productAndConsumer = productAndConsumer;
}
@Override
public void run() {
while (true) {
productAndConsumer.product();
try {
// 注意:这里不进行睡眠的话,不能展示出,装弹一次射击一次的现象
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private static class ConsumerThread extends Thread {
public ProductAndConsumer productAndConsumer;
public ConsumerThread(ProductAndConsumer productAndConsumer) {
this.productAndConsumer = productAndConsumer;
}
@Override
public void run() {
while (true) {
productAndConsumer.consumer();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public int getLimitCount() {
return limitCount;
}
public void setLimitCount(int limitCount) {
this.limitCount = limitCount;
}
}
等待超时模式实现一个连接池
连接池类
package com.concurrent.ch1.pool;
import java.sql.Connection;
import java.util.LinkedList;
/**
* @author h
* @Description 连接池的实现
* @createTime 2020年03月14日 22:19
*/
public class DBPool {
private static LinkedList<Connection> pool = new LinkedList<>();
public DBPool(int initialSize) {
if (initialSize > 0) {
for (int i = 0; i < initialSize; i++) {
pool.addLast(SqlConnectImpl.fetchConnection());
}
}
}
/**
* 释放连接
*
* @param connection
*/
public void releaseConnection(Connection connection) {
if (null != connection) {
synchronized (pool) {
// 把释放的连接添加回池中
pool.addLast(connection);
// 连接池中有连接后,唤醒处于等待的线程
pool.notifyAll();
}
}
}
/**
* 获取连接,在mills内无法获取到连接,将会返回null
*
* @param mills 超时时间,小于0时表示永不超时
* @return
*/
public Connection fetchConnection(long mills) throws InterruptedException {
synchronized (pool) {
// 不设置超时时间的情况
if (mills <= 0) {
while (pool.isEmpty()) {
// 连接池中没有连接,进入等待状态。
System.out.println("连接池中没有连接,进入等待状态。");
pool.wait();
}
// 取出返回并移除第一个位置的连接
return pool.removeFirst();
} else {
// 超时的具体时间
long future = System.currentTimeMillis() + mills;
// 剩下等待的时间
long remaining = mills;
// 如果条件不满足,要进入等待状态
while (pool.isEmpty() && remaining > 0) {
System.out.println("连接池中没有连接,进入等待状态。");
// 等待指定时长
pool.wait(remaining);
// 可能在指定的等待时长时间内线程被唤醒,所以需要实时计算剩余的等待时间(可能会被唤醒多次)
remaining = future - System.currentTimeMillis();
}
Connection connection = null;
if(!pool.isEmpty()){
System.out.println("成功获取连接。");
connection = pool.removeFirst();
}
return connection;
}
}
}
}
测试类
package com.concurrent.ch1.pool;
import java.sql.Connection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author h
* @Description 线程池测试类
* @createTime 2020年03月14日 23:26
*/
public class TestDBPool {
private static DBPool dbPool = new DBPool(1);
public static void main(String[] args) throws InterruptedException {
// 线程总数量
int threadCount = 50;
CountDownLatch end = new CountDownLatch(threadCount);
// 记录成功获取线程的数量
AtomicInteger get = new AtomicInteger(0);
AtomicInteger notGet = new AtomicInteger(0);
// 每个线程获取连接的次数
int count = 20;
for (int i = 0; i < threadCount; i++) {
Work work = new Work(count, get, notGet, end);
work.start();
}
end.await();
System.out.println("总共尝试获取" + (threadCount * count));
System.out.println("获取成功" + get);
System.out.println("获取失败" + notGet);
}
// 消费者
private static class Work extends Thread {
int count;
AtomicInteger get;
AtomicInteger notGet;
CountDownLatch end;
public Work(int count, AtomicInteger get, AtomicInteger notGet, CountDownLatch end) {
this.count = count;
this.get = get;
this.notGet = notGet;
this.end = end;
}
@Override
public void run() {
while (count > 0) {
try {
// 1.获取连接
Connection connection = dbPool.fetchConnection(1);
if (connection != null) {
try {
// 2.模拟数据库操作
connection.createStatement();
connection.commit();
} finally {
// 3.释放连接
dbPool.releaseConnection(connection);
get.incrementAndGet();
}
} else {
notGet.incrementAndGet();
System.out.println("等待超时,未获取到连接");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
count--;
}
}
end.countDown();
}
}
}
数据库连接类 (这里只复制了部分代码)
package com.concurrent.ch1.pool;
import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
/**
* @author h
* @Description TODO
* @createTime 2020年03月14日 22:22
*/
public class SqlConnectImpl implements Connection {
public static final Connection fetchConnection(){
return new SqlConnectImpl();
}
@Override
public Statement createStatement() throws SQLException {
return null;
}
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
return null;
}
}
6.3 调用yield()、sleep() 、wait()、notify() 等方法对锁有何影响?
yield():调用yield() 方法后,只是让出CPU的执行权,不会释放当前线程持有的锁。
sleep():也不会释放当前线程持有的锁。例如:当前线程持有锁并且在同步代码块中使用了sleep() 方法,其他线程需要等待休眠完和当前线程同步代码块执行完才会释放锁。
wait():被调用之后,会释放当前线程持有的锁,当被唤醒时当前线程后,会重新去竞争锁,抢到锁后才往后执行wait()后面的代码。
notify() / notifyAll():被调用之后,不会释放锁,所以一般放在同步代码块的最后一行。