全文较长,建议收藏!
JUC(java.util.concurrent)
JUC具体指java.util.concurrent包相关内容,包含java.util.concurrent.lock java.util.concurrent.atomic相关内容。下文是在学习过程中的一些问题以及查询资料找到的面试问题
如何理解进程和线程?
如我们使用的QQ,双击启动QQ.exe就是启动了一个进程,通过QQ和A聊天是一个线程,同事跟B聊天又是一个线程。
JAVA无法创建线程,是通过调用native方法(C++)来创建线程,JAVA无法操作硬件
一个正常的JAVA类通常会有两个线程,main方法,gc线程
线程有几种状态?
Thread.State枚举中列举了线程的6种状态
NEW-新建,新生
RUNNABLE-运行
BLOCKED-阻塞
WAITING-等待
TIMED_WAITING-超时等待
TERMINATED-终止
Wait和sleep区别?
Wait是Object类的,sleep是Thread类的,但在正常开发过程中不会用到sleep方法,一般使用JUC下的TimeUnit工具类实现sleep
Wait会释放锁,sleep不会释放锁
Wait只能在同步代码块中使用,sleep可以在任何地方睡
Wait不需要捕获异常,sleep需要捕获异常(超时等待异常)(这里只是针对于这两个方法,但是实际使用中会发现wait也需要捕获一个异常,线程中断异常是所有线程都会有的)
创建线程的三种方式?
Thread,Runnable,Callable
正式生产中前两种方式并不常用
Runnable是一个@ FunctionalInterface 函数式接口,jdk1.8后使用lambda表达式创建 new Thread((参数)->{代码},“A”).start();
new Thread(()->{ for (int i = 0; i < 100; i++) gzl.sale(); },"C").start();
Lock锁与Synchronized
线程是一个单独的资源类,不需要任何附属操作,线程的解耦合:资源类中包含需要的属性、方法,然后再进行实际调用
package com.gu;
public class Test1 {
public static void main(String[] args) {
Gzl gzl = new Gzl();
new Thread(()->{
for (int i = 0; i < 100; i++) {
gzl.sale();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 100; i++) {
gzl.sale();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 100; i++) {
gzl.sale();
}
},"C").start();
}
}
class Gzl{
private int a = 60;
public synchronized void sale(){
if (a>0) {
System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"剩余:"+a);
}
}
}
Synchronized是最传统的锁,实际意义是队列,锁,是非公平锁
什么是公平锁,什么是非公平锁?
公平锁:十分公平,先来后到
非公平锁:不公平,根据CPU调度来选择顺序
Lock锁常用方法
.lock()加锁,.unlock()解锁,.trylock()尝试获取锁
package com.gu;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test2 {
public static void main(String[] args) {
Gzl gzl = new Gzl();
new Thread(()->{ for (int i = 0; i < 100; i++) gzl.sale(); },"A").start();
new Thread(()->{ for (int i = 0; i < 100; i++) gzl.sale(); },"B").start();
new Thread(()->{ for (int i = 0; i < 100; i++) gzl.sale(); },"C").start();
}
}
class Gzl2{
private int a = 60;
public void sale(){
Lock lock = new ReentrantLock();
// 加锁
lock.lock();
try {
if (a>0) {
System.out.println(Thread.currentThread().getName()+"卖出"+(a--)+"剩余:"+a);
}
}catch (Exception e){
e.printStackTrace();
}finally {
// 解锁
lock.unlock();
}
}
}
拥有三个实现类:
ReentrantLock可重入锁:默认是非公平锁,可传递参数设置为公平锁(Boolean)
ReentrantReadWriteLock.Readlock读锁,ReentrantReadWriteLock.Writelock写锁
Synchronized和Lock区别?
1.Synchronized是一个内置的java关键字,lock是一个java接口
2.Synchronized无法判断锁的状态,lock可以判断是否获取到了锁
3.Synchronized会自动释放锁,lock不会自动释放,必须手动解锁,如果不释放会产生死锁
4.Synchronized是阻塞锁,lock是非阻塞锁
5.Synchronized是可重入锁,不可中断的,非公平锁,lock锁是可重入锁,可以判断锁,可以设置是否是非公平锁
6.Synchronized适合锁少量的代码同步问题,lock适合锁大量的同步代码
线程中存在的生产者消费者问题:等待唤醒,通知唤醒
Synchronized解决方案
package com.gu;
/**
* 线程中存在的生产者消费者问题:等待唤醒,通知唤醒
* 线程交替执行,多个线程操作一个变量
* 三个关键词:判断等待,执行业务,发布通知
*/
public class Test3 {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 50; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 50; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 50; i++) {
try {
data.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 50; i++) {
try {
data.decrement();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"D").start();
}
}
class Data{
private int number = 0;
// +1
public synchronized void increment() throws InterruptedException {
while (number != 0){
// 等待
this.wait();
}
// if (number != 0){
// // 等待,在多线程等待,通知时禁止使用if作为判断条件,一定要使用while判断,否则会出现虚假唤醒问题
// this.wait();
// }
number ++;//业务
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我-1完毕了
this.notifyAll();
}
// -1
public synchronized void decrement() throws InterruptedException {
while (number == 0){
// 等待
this.wait();
}
// if (number == 0){
// // 等待,在多线程等待,通知时禁止使用if作为判断条件,一定要使用while判断,否则会出现虚假唤醒问题
// this.wait();
// }
number --;//业务
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我-1完毕了
this.notifyAll();
}
}
上图是jdk官网文档描述的虚假唤醒问题解决方案
Lock解决方案(JUC版本)-非精准唤醒
package com.gu;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test4 {
public static void main(String[] args) {
Data1 data = new Data1();
new Thread(()->{
for (int i = 0; i < 50; i++) {
data.increment();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 50; i++) {
data.decrement();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 50; i++) {
data.increment();
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 50; i++) {
data.decrement();
}
},"D").start();
}
}
class Data1{
private int number = 0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
// +1
public void increment() {
lock.lock();
try {
while (number != 0){
// 等待
condition.await();
}
number ++;//业务
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我-1完毕了
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// -1
public void decrement() {
lock.lock();
try {
while (number == 0){
// 等待
condition.await();
}
number --;//业务
System.out.println(Thread.currentThread().getName()+"=>"+number);
// 通知其他线程,我-1完毕了
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
Condition与wait sleep的区别?
Condition与wait sleep的最大区别,Condition可以精确的通知和唤醒线程
Lock解决方案(JUC版本)-精准唤醒
package com.gu;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Test5 {
public static void main(String[] args) {
Data2 data = new Data2();
new Thread(()->{
for (int i = 0; i < 50; i++) {
data.printThreadA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 50; i++) {
data.printThreadB();
}
},"B").start();
new Thread(()->{
for (int i = 0; i < 50; i++) {
data.printThreadC();
}
},"C").start();
new Thread(()->{
for (int i = 0; i < 50; i++) {
data.printThreadD();
}
},"D").start();
}
}
class Data2{
// A=1 B=2 C=3 D=4
private int number = 1;
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private Condition condition4 = lock.newCondition();
public void printThreadA(){
lock.lock();
try {
while (number != 1){
// 等待
condition1.await();
}
// 业务
number = 2;
System.out.println(Thread.currentThread().getName()+"=>AAAAAAAAA");
//唤醒
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printThreadB(){
lock.lock();
try {
while (number != 2){
// 等待
condition2.await();
}
// 业务
number = 3;
System.out.println(Thread.currentThread().getName()+"=>BBBBBBBBBB");
//唤醒
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printThreadC(){
lock.lock();
try {
while (number != 3){
// 等待
condition3.await();
}
// 业务
number = 4;
System.out.println(Thread.currentThread().getName()+"=>CCCCCCCCCCC");
//唤醒
condition4.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printThreadD(){
lock.lock();
try {
while (number != 4){
// 等待
condition4.await();
}
// 业务
number = 1;
System.out.println(Thread.currentThread().getName()+"=>DDDDDDDDD");
//唤醒
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
锁的内存语义
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A 对共享变量所做修改的)消息。
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共 享变量所做修改的)消息。
线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。
八锁现象
八锁指的是关于深入理解锁的概念的八个问题,下面用4段代码进行描述
八锁问题1、2
package com.gu.lock8;
import java.util.concurrent.TimeUnit;
/**
* 8锁问题 1 2
* 1,标准情况下,两个线程先打印发短信还是打电话。 答:发短信
* (synchronized锁的是对象的调用者<可以理解为锁的一个对象实例>,由于是A线程先使用phone去调用的sendSms,
* 所以他先拿到的锁,无论sendSms要执行多久,都会等待他执行完毕之后在执行后续方法)
*
* 2,sendSms睡眠4秒,两个线程先打印发短信还是打电话。 答:发短信
* (synchronized锁的是对象的调用者<可以理解为锁的一个对象实例>,由于是A线程先使用phone去调用的sendSms,
* 所以他先拿到的锁,无论sendSms要执行多久,都会等待他执行完毕之后在执行后续方法)
*
*/
public class Lock1 {
public static void main(String[] args) throws InterruptedException {
Phone phone = new Phone();
new Thread(()->{
phone.sendSms();
},"A").start();
// JUC下工具类,用来处理线程睡眠问题
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone.call();
},"B").start();
}
}
// 原型类
class Phone{
public synchronized void sendSms(){
// JUC下工具类,用来处理线程睡眠问题
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"=>发短信");
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName()+"=>打电话");
}
}
八锁问题3、4
package com.gu.lock8;
import java.util.concurrent.TimeUnit;
/**
* 8锁问题 3 4
* 3,新增一个非同步方法,两个线程先打印发短信还是hello。 答:hello
* (synchronized锁的是对象的调用者<可以理解为锁的一个对象实例>,由于是A线程先使用phone去调用的sendSms,
* 所以他先拿到的锁,无论sendSms要执行多久,都会等待他执行完毕之后在执行后续方法,但是如果有非同步方法,则非同步方法不存在锁的问题,则先执行非同步方法)
*
* 4,创建两个对象那个,两个线程分别操作两个对象打印发短信还是打电话。 答:打电话
* (synchronized锁的是对象的调用者<可以理解为锁的一个对象实例>,由于是A线程先使用phone去调用的sendSms,
* 所以他先拿到的锁,无论sendSms要执行多久,都会等待他执行完毕之后在执行后续方法,但是如果new两个对象,则先执行打电话)
*
*/
public class lock2 {
public static void main(String[] args) throws InterruptedException {
Phone2 phone1 = new Phone2();
Phone2 phone2 = new Phone2();
new Thread(()->{
phone1.sendSms();
},"A").start();
// JUC下工具类,用来处理线程睡眠问题
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone2.call();
},"B").start();
}
}
// 原型类
class Phone2{
public synchronized void sendSms(){
// JUC下工具类,用来处理线程睡眠问题
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"=>发短信");
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName()+"=>打电话");
}
/**
* 非同步方法不受影响
*/
public void hello(){
System.out.println(Thread.currentThread().getName()+"=>hello");
}
}
八锁问题5、6
package com.gu.lock8;
import java.util.concurrent.TimeUnit;
/**
* 8锁问题 5 6
* 5,加上static变为静态方法之后,两个线程先打印发短信还是打电话。 答:发短信
* (synchronized锁的是对象的调用者<可以理解为锁的一个对象实例>,由于是A线程先使用phone去调用的sendSms,
* 所以他先拿到的锁,无论sendSms要执行多久,都会等待他执行完毕之后在执行后续方法,虽然是静态方法,但是由于操作的同一个Class类模板,还是先执行发短信)
*
* 6,加上static变为静态方法之后,并且使用两个对象通过两个线程先打印发短信还是打电话。 答:发短信
* (synchronized锁的是对象的调用者<可以理解为锁的一个对象实例>,由于是A线程先使用phone去调用的sendSms,
* 所以他先拿到的锁,无论sendSms要执行多久,都会等待他执行完毕之后在执行后续方法,虽然是静态方法,并且两个对象,
* 但是由于操作的同一个Class类模板,还是先执行发短信)
*
*/
public class lock3 {
public static void main(String[] args) throws InterruptedException {
// 两个对象,但是是同一个Class类模板
Phone3 phone1 = new Phone3();
Phone3 phone2 = new Phone3();
new Thread(()->{
phone1.sendSms();
// Phone3.sendSms(); 两种方法是一样的,因为无论是new的实例,还是直接使用类名调用,静态方法在加载时就已经有了,也就加锁了
},"A").start();
// JUC下工具类,用来处理线程睡眠问题
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone2.call();
},"B").start();
}
}
// 原型类
class Phone3{
/**
* 加上static之后,锁的就不是调用者(实例),而是Class
*/
public static synchronized void sendSms(){
// JUC下工具类,用来处理线程睡眠问题
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"=>发短信");
}
public static synchronized void call(){
System.out.println(Thread.currentThread().getName()+"=>打电话");
}
}
八锁问题7、8
package com.gu.lock8;
import java.util.concurrent.TimeUnit;
/**
* 8锁问题 7 8
* 7,一个static同步方法,一个普通同步方法,两个线程先打印发短信还是打电话。 答:打电话
* (synchronized锁的是对象的调用者<可以理解为锁的一个对象实例>,由于是A线程先使用phone去调用的sendSms,
* 所以他先拿到的锁,无论sendSms要执行多久,都会等待他执行完毕之后在执行后续方法,两个方法并不是一个锁,static同步方法锁Class模板,普通同步方法锁对象)
*
* 8,一个static同步方法,一个普通同步方法,并且使用两个对象通过两个线程先打印发短信还是打电话。 答:打电话
* (synchronized锁的是对象的调用者<可以理解为锁的一个对象实例>,由于是A线程先使用phone去调用的sendSms,
* 所以他先拿到的锁,无论sendSms要执行多久,都会等待他执行完毕之后在执行后续方法,两个方法并不是一个锁,static同步方法锁Class模板,普通同步方法锁对象)
*
*/
public class lock4 {
public static void main(String[] args) throws InterruptedException {
// 两个对象,但是是同一个Class类模板
Phone4 phone1 = new Phone4();
Phone4 phone2 = new Phone4();
new Thread(()->{
phone1.sendSms();
// Phone3.sendSms(); 两种方法是一样的,因为无论是new的实例,还是直接使用类名调用,静态方法在加载时就已经有了,也就加锁了
},"A").start();
// JUC下工具类,用来处理线程睡眠问题
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone2.call();
},"B").start();
}
}
// 原型类
class Phone4{
/**
* 加上static之后,锁的就不是调用者(实例),而是Class
*/
public static synchronized void sendSms(){
// JUC下工具类,用来处理线程睡眠问题
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"=>发短信");
}
public synchronized void call(){
System.out.println(Thread.currentThread().getName()+"=>打电话");
}
}
总结
对于synchronized锁来说,如果锁普通方法,锁的是对象,如果是锁的静态方法,锁的是Class模板
通过上面的几段示例可以清晰的理解锁的到底是什么
集合类不安全(List)
Java.Util.ConcurrentModificationException异常:并发修改异常,多线程下使用arraylist等不安全集合会出现
创建list的方式
package com.gu.unsafe;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Test1 {
public static void main(String[] args) {
// 创建list的两种方式
List<String> list = Arrays.asList("A","B","C");
list.forEach(item-> System.out.println("list"+item));
// 可以简写为list.forEach(System.out::println;
List<String> list1 = new ArrayList<String>();
list1.add("A");
list1.add("B");
list1.add("C");
for (String a: list1) {
System.out.println("list1" + a);
}
}
}
三种线程安全的list
List list1 = new Vector<>();
这个是最简单的线程安全的list,与arraylist及LinkedList的区别如下:
1、ArrayList和Vector是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要讲已经有数组的数据复制到新的存储空间中。当从中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。
都适合随机查找和遍历,不适合插入和删除,但是Vector底层加上了synchronized关键字,所以他的效率不如arraylist,arraylist扩容为50%,Vector为100%,所以在数据量较大的使用Vector有一定优势
2、LinkedList是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。
List list1 = Collections.synchronizedList(new ArrayList<>());
Collections.synchronizedList方法可以将一个List包装为同步(线程安全)的List。但是通过Iterator(迭代器)、Spliterator(分割)或Stream(流)遍历这个新List时,用户去自己做同步处理。因为同步实现方式相同,所以包装后的List性能与Vector相当。
List list1 = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList add(E) 和remove(int index)都是对新的数组进行修改和新增。所以在多线程操作时不会出现java.util.ConcurrentModificationException错误。
CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。发生修改时候做copy,新老版本分离,保证读的高性能,在写操作时会进行copy,所以更适用于以读为主的情况。
三中list的示例
package com.gu.unsafe;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 使用线程安全list的三种方法
*/
public class ListTest1 {
public static void main(String[] args) {
/**
* 多线程下使list变为线程安全的方法
* 1,List<String> list1 = new Vector<>();
* 2,List<String> list1 = Collections.synchronizedList(new ArrayList<>());
* 3,List<String> list1 = new CopyOnWriteArrayList<>();
*/
List<String> list1 = new CopyOnWriteArrayList<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
list1.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(Thread.currentThread().getName()+"=>"+list1.toString());
},String.valueOf(i)).start();
}
new Thread(()->{
while (list1.size() != 10){
}
list1.forEach(System.out::println);
},"A").start();
}
}
集合类不安全(Set)
两种线程安全的set
Set没有其他可取代的方式,所以只有两种将set转换为线程安全的方式:
使用并发工具类Collections的synchronizedSet将set转换为线程安全的set
使用JUC包下的CopyOnWriteArraySet来保证线程安全,与CopyOnWriteArrayList相同,也是在底层使用了Lock锁
package com.gu.unsafe;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* 使用线程安全Set的两种方法
*/
public class SetTest1 {
public static void main(String[] args) {
/**
* 默认创建方式: Set<String> set = new HashSet<String>();
* 多线程下使Set变为线程安全的方法
* 1,Set<String> set = Collections.synchronizedSet( new HashSet<String>());
* 2,Set<String> set = new CopyOnWriteArraySet<>();
*/
// Set<String> set = new HashSet<String>();
// Set<String> set = Collections.synchronizedSet( new HashSet<String>());
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 1; i <= 10; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(Thread.currentThread().getName()+"=>"+set);
},String.valueOf(i)).start();
}
}
}
HashSet的底层
set的本质其实就是map key是无法重复的
集合类不安全(Map)
HashMap的存储结构
紫色部分即代表哈希表,也称为哈希数组(默认数组大小是16,每对key-value键值对其实是存在map的内部类entry里的),数组的每个元素都是一个单链表的头节点,跟着的绿色链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中。
举例说明的话可以简单理解hashmap的hash算法为取余,1和9都是余1,那么他们就会在数组的同一位置,然后将其放入单链表中。
hashmap原理(即put和get原理)
put原理
根据key获取对应hash值:int hash = hash(key.hash.hashcode())
根据hash值和数组长度确定对应数组:int i = indexFor(hash, table.length); 简单理解就是i = hash值%数组长度(其实是按位与运算)。如果不同的key都映射到了数组的同一位置处,就将其放入单链表中。且新来的是放在头节点。
get原理
通过hash获得对应数组位置,遍历该数组所在链表(key.equals())
hashcode相同,冲突怎么办?
“头插法”,放到对应的链表的头部。
为什么是头插法(其设计原理是什么)?
因为HashMap的发明者认为,后插入的Entry被查找的可能性更大(因为get查询的时候会遍历整个链表)。
hashmap的初始容量及扩容,加载因子
默认初始容量为16,加载因子为0.75,扩容后的 HashMap 容量是之前容量的两倍
为啥初始容量要16或者2的幂次?
若不是16或者2的幂次,位运算的结果不够均匀分布,显然不符合Hash算法均匀分布的原则。
反观长度16或者其他2的幂,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。
hashmap在并发时会出现什么问题?
hashmap在接近临界点时,若此时两个或者多个线程进行put操作,都会进行resize(扩容)和ReHash(为key重新计算所在位置),而ReHash在并发的情况下可能会形成链表环(环形锁)。
如何判断有环形表?
首先创建两个指针A和B(在java里就是两个对象引用),同时指向这个链表的头节点。然后开始一个大循环,在循环体中,让指针A每次向下移动一个节点,让指针B每次向下移动两个节点,然后比较两个指针指向的节点是否相同。如果相同,则判断出链表有环,如果不同,则继续下一次循环。
此方法也可以用一个更生动的例子来形容:在一个环形跑道上,两个运动员在同一地点起跑,一个运动员速度快,一个运动员速度慢。当两人跑了一段时间,速度快的运动员必然会从速度慢的运动员身后再次追上并超过,原因很简单,因为跑道是环形的。
hashmap和hashtable的区别
concurrenthashmap的存储结构
hashmap是有entry数组组成,而concurrenthashmap则是Segment数组。那Segment是什么呢?Segment本身就相当于一个HashMap对象。同HashMap一样,Segment包含一个HashEntry数组,数组中的每一个HashEntry既是一个键值对,也是一个链表的头节点。
单一的Segment结构如下(是不是看着就是hashmap):
像这样的Segment对象,在ConcurrentHashMap集合中有多少个呢?有2的N次方个,共同保存在一个名为segments的数组当中。
可以说,ConcurrentHashMap是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表。(这样类比理解hashmap)
put和get方法对比hashmap的put和get方法?
Put方法:
1.为输入的Key做Hash运算,得到hash值。
2.通过hash值,定位到对应的Segment对象
3.获取可重入锁
4.再次通过hash值,定位到Segment当中数组的具体位置。
5.插入或覆盖HashEntry对象。
6.释放锁。
Get方法:
1.为输入的Key做Hash运算,得到hash值。
2.通过hash值,定位到对应的Segment对象
3.再次通过hash值,定位到Segment当中数组的具体位置。
由此可见,和hashmap相比,ConcurrentHashMap在读写的时候都需要进行二次定位。先定位到Segment,再定位到Segment内的具体数组下标。
concurrenthashmap和hashtable都是线程安全,但是前者性能更高
因为前者是用的分段锁,根据hash值锁住对应Segment对象,当hash值不同时,使其能实现并行插入,效率更高,而hashtable则会锁住整个map。
如何理解并行插入:当cmap需要put元素的时候,并不是对整个map进行加锁,而是先通过hashcode来知道他要放在那一个分段(Segment对象)中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在同一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计(即效率稍低)。
分段锁的设计解决的是什么问题?
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一部分行加锁操作。
jdk1.8后Hashmap,ConcurrentHashMap与之前版本的不同
JDK1.7的hashmap和JDK1.8的hashmap的区别?
1.为了加快查询效率,java8的hashmap引入了红黑树结构,当数组长度大于默认阈值64时,且当某一链表的元素>8时,该链表就会转成红黑树结构,查询效率更高。(问题来了,什么是红黑树?什么是B+树?(mysql索引有B+树索引)什么是B树?什么是二叉查找树?)数据结构方面的知识点会更新在【数据结构专题】,这里不展开。
红黑树是一种自平衡二叉树,拥有优秀的查询和插入/删除性能,广泛应用于关联数组。
对比AVL树,AVL要求每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1,而红黑树通过适当的放低该条件(红黑树限制从根到叶子的最长的可能路径不多于最短的可能路径的两倍长,结果是这个树大致上是平衡的),以此来减少插入/删除时的平衡调整耗时,从而获取更好的性能,而这虽然会导致红黑树的查询会比AVL稍慢,但相比插入/删除时获取的时间,这个付出在大多数情况下显然是值得的。
2.优化扩容方法,在扩容时保持了原来链表中的顺序,避免出现死循环
JDK1.7的concurrenthashmap和JDK1.8的区别?
1.8的实现已经抛弃了Segment分段锁机制,利用Node数组+CAS+Synchronized来保证并发更新的安全,底层采用数组+链表+红黑树的存储结构。还需要继续理解
Callable
创建线程的2种方式,一种是直接继承Thread,另外一种就是实现Runnable接口。
这2种方式都有一个缺陷就是:在执行完任务之后无法获取执行结果。
如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。
而自从Java 1.5开始,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。
package com.gu.callable;
import java.util.concurrent.*;
/**
* Callable测试类
* @date 2020-03-13
* @author gzl
*/
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
School school = new School();
FutureTask futureTask1 = new FutureTask<String>(school);
FutureTask futureTask2 = new FutureTask<String>(school);
FutureTask futureTask3 = new FutureTask<String>(school);
/**
* FutureTask 创建多个的时候可以多个线程多次执行同一个方法,否则只会执行一次,因为是操作的同一个FutureTask对象
* 这个方法也是我能想到最简单的写法
*/
new Thread(futureTask1,"A").start();
new Thread(futureTask2,"B").start();
new Thread(futureTask3,"C").start();
/**
* get方法会返回Callable接口的返回值,他有两个重写方法
* 无参:默认阻塞,等待方法执行结束返回
* 有参:通过public V get(long timeout, TimeUnit unit),设置超时时间,如果超时未返回则返回null(但我没试出来,并且报错)
* 报错信息:Exception in thread "main" java.util.concurrent.TimeoutException
* at java.util.concurrent.FutureTask.get(FutureTask.java:205)
* at com.gu.callable.CallableTest.main(CallableTest.java:15)
*/
System.out.println(futureTask1.get());
System.out.println(futureTask2.get());
System.out.println(futureTask3.get());
}
}
class School implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("进入了call方法");
return "1";
}
}
Future
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
Future类位于java.util.concurrent包下,它是一个接口:
在Future接口中声明了5个方法,下面依次解释每个方法的作用:
cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
isDone方法表示任务是否已经完成,若任务完成,则返回true;
get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。
也就是说Future提供了三种功能:
1)判断任务是否完成;
2)能够中断任务;
3)能够获取任务执行结果。
因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。
FutureTask
FutureTask是Future接口的一个唯一实现类。
FutureTask类实现了RunnableFuture接口,我们看一下RunnableFuture接口的实现:
可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。
所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
Runnable和Callable的区别
(1)Callable规定的方法是call(),Runnable规定的方法是run()
(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
(3)call方法可以抛出异常,run方法不可以
常用的辅助类
CountDownLatch
上面是jdk官方给出的解释,实际意义可以认为,必须要执行任务的时候再使用的减法计数器
用法如下:
package com.gu.add;
import java.util.concurrent.CountDownLatch;
/**
* 计数器 -减法
*/
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
/**
* 创建计数器
* 总数是10,必须要执行任务的时候再使用
*/
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 1; i <= 10; i++) {
new Thread(()->{
System.out.println("我进来了!" + Thread.currentThread().getName());
// 数量-1
countDownLatch.countDown();
},String.valueOf(i)).start();
}
// 等待计数器归零,再向下执行
countDownLatch.await();
System.out.println("关门放狗!");
}
}
原理:
countDownLatch.countDown(); 数量-1
countDownLatch.await(); 等待计数器归零之后,再向下执行
每次线程调用countDownLatch.countDown();数量-1,假设计数器变为0,countDownLatch.await();就会被唤醒,继续执行后续内容。
CyclicBarrier
上面是jdk官方给出的解释,实际意义可以认为,等待执行线程数到第10个时触发
用法如下:
package com.gu.add;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
/**
* 计数器 +加法
*/
public class CyclicBarrierDemo {
public static void main(String[] args) {
/**
* 创建一个加法计数器
* 拥有两个构造函数:public CyclicBarrier(int parties, Runnable barrierAction) 设置数值,以及达到该数值之后触发的线程
* public CyclicBarrier(int parties) 设置数值
*/
CyclicBarrier cyclicBarrier = new CyclicBarrier(10,()->{
System.out.println("起飞");
});
for (int i = 1; i <= 10; i++) {
final int temp = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "激活了" + temp + "个核心");
try {
cyclicBarrier.await();
// cyclicBarrier.await(1,TimeUnit.SECONDS); 也可以通过这个设置超时等待时间
} catch (InterruptedException e) {
// 当阻塞方法收到中断请求的时候就会抛出InterruptedException异常
e.printStackTrace();
} catch (BrokenBarrierException e) {
// 屏障被破坏异常
e.printStackTrace();
}
}).start();
}
}
}
可以简单理解为加法计数器,与上面减法计数器用法大致相同
Semaphore
上面是jdk官方给出的解释,实际意义可以认为,限制同时进行的线程数量(限流)
用法如下:
package com.gu.add;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
* 信号量 限流
* 举例如:停车位
*/
public class SemaphoreDemo {
public static void main(String[] args) {
// 同时进行的线程数量 停车位:5,车辆:10
Semaphore semaphore = new Semaphore(5);
for (int i = 1; i <= 10; i++) {
new Thread(()->{
try {
// 得到,拿到车位
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到车位了!");
// 停车
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "离开车位了!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 放回,放开车位
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
大概原理:
semaphore.acquire(); 获得,假设如果已经满了,等待,直到被释放为止;
semaphore.release(); 释放,会将当前的信号量释放+1,然后唤醒被等待的线程;
semaphore的作用:多个线程资源互斥;
并发限流,控制最大同时执行的线程数量;
读写锁
ReadWriteLock
上面是jdk官方给出的解释,该类与lock锁用法大致相同,不过需要通过两个方法调用readWriteLock.writeLock().lock(); readWriteLock.readLock().lock();
分别用于读场景和写场景,如高并发下缓存的写入及读取,是一个典型的乐观锁场景
用法如下:
package com.gu.rw;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 读写锁:读锁可以多个线程同时进行,写锁为独占锁,只能有一个线程来操作
* 读 读:可以共存
* 读 写:不可同存
* 写 写:不可同存
* 独占锁:顾名思义,一次只能被一个线程占有
* 共享锁:顾名思义,一次能被多个线程同时使用
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
/**
* 非读写锁场景
*/
// MyCache myCache = new MyCache();
/**
* 读写锁场景
*/
MyCacheLock myCache = new MyCacheLock();
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
// 调用写入缓存方法
myCache.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
for (int i = 1; i <= 5; i++) {
final int temp = i;
new Thread(()->{
// 调用读取缓存方法
myCache.get(temp+"");
},String.valueOf(i)).start();
}
}
}
class MyCacheLock{
private volatile Map<String,Object> map = new HashMap<>();
// 创建读写锁
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/**
* 保存缓存
* @param key Key
* @param value Value
*/
public void put(String key,Object value){
// 写锁 加锁,只允许一个线程写
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "写入了缓存:" + value);
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "写入缓存:" + value + "完毕!");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 写锁 解锁
readWriteLock.writeLock().unlock();
}
}
/**
* 读取缓存
*/
public void get(String key){
// 读锁,允许多个线程同时读
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "读取了缓存");
map.get(key);
System.out.println(Thread.currentThread().getName() + "读取缓存:完毕!");
} catch (Exception e) {
e.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
}
class MyCache{
private volatile Map<String,Object> map = new HashMap<>();
/**
* 保存缓存
* @param key Key
* @param value Value
*/
public void put(String key,Object value){
System.out.println(Thread.currentThread().getName() + "写入了缓存:" + value);
map.put(key,value);
System.out.println(Thread.currentThread().getName() + "写入缓存:" + value + "完毕!");
}
/**
* 读取缓存
*/
public void get(String key){
System.out.println(Thread.currentThread().getName() + "读取了缓存");
map.get(key);
System.out.println(Thread.currentThread().getName() + "读取缓存:完毕!");
}
}
- 读写锁:读锁可以多个线程同时进行,写锁为独占锁,只能有一个线程来操作
- 独占锁:顾名思义,一次只能被一个线程占有
- 共享锁:顾名思义,一次能被多个线程同时使用
阻塞队列
如何理解阻塞和队列?
阻塞队列:Interface BlockingQueue
阻塞队列的使用场景
多线程并发处理,线程池,下方是jdk官方的解释
阻塞队列的使用
添加,移除
队列有四组api:
1,抛出异常
2,不抛出异常
3,阻塞等待
4,超时等待
方式 | 抛出异常 | 不抛出异常 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add() | offer() | put() | offer(,) 有参 |
移除 | remove() | poll() | take() | poll(,)有参 |
判断队列首 | element() | peek() | - | - |
package com.gu.bq;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) throws InterruptedException {
// List SET Queue 父类都是Collection
// 抛出异常
test1();
// 不抛出异常
test2();
// 阻塞等待
test3();
// 超时等待
test4();
}
/**
* 队列的使用:抛出异常
* IllegalStateException: Queue full 队列满了之后再进行插入会报错,队列已满
* NoSuchElementException 如果队列为空还去进行取,会报错,队列没有元素
* 如果队列为空,去查询首元素,会报错:NoSuchElementException 队列没有元素
*/
public static void test1(){
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(3);
System.out.println(arrayBlockingQueue.add("A"));
System.out.println(arrayBlockingQueue.add("B"));
System.out.println(arrayBlockingQueue.add("C"));
// 如果队列为空,去查询首元素,会报错:NoSuchElementException 队列没有元素
System.out.println(arrayBlockingQueue.element());
// 队列满了之后再进行插入会报错,队列已满
// System.out.println(arrayBlockingQueue.add("D"));
// remove方法传参时不会报错,如果元素在队列中存在会返回true,不存在则返回false,如果队列为空则也是返回false
// System.out.println(arrayBlockingQueue.remove("A"));
// System.out.println(arrayBlockingQueue.remove("B"));
// System.out.println(arrayBlockingQueue.remove("C"));
//
// System.out.println(arrayBlockingQueue.remove("D"));
// remove方法不传参时会报错,如果元素再队列中存在会返回true,不存在或者队列为空则报错 NoSuchElementException
System.out.println(arrayBlockingQueue.remove());
System.out.println(arrayBlockingQueue.remove());
System.out.println(arrayBlockingQueue.remove());
System.out.println(arrayBlockingQueue.remove());
}
/**
* 队列的使用:不抛出异常
* 添加如果为队列已满则返回 false
* 移除如果队列为空则返回 null
* 如果队列为空,去查询首元素则返回 null
*/
public static void test2(){
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(3);
System.out.println(arrayBlockingQueue.offer("A"));
System.out.println(arrayBlockingQueue.offer("B"));
System.out.println(arrayBlockingQueue.offer("C"));
// 如果队列为空,去查询首元素则返回 null
System.out.println(arrayBlockingQueue.peek());
// 添加如果为队列已满则返回 false
// System.out.println(arrayBlockingQueue.offer("D"));
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
// 移除如果队列为空则返回 null
System.out.println(arrayBlockingQueue.poll());
}
/**
* 队列的使用:一直阻塞
* 队列满了之后再进行插入会一直阻塞,直到队列中有元素被取出
* 队列为空之后再进行取出会一直阻塞,直到队列中放入新的元素
*/
public static void test3() throws InterruptedException {
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(3);
arrayBlockingQueue.put("A");
arrayBlockingQueue.put("B");
arrayBlockingQueue.put("C");
// 队列满了之后再进行插入会一直阻塞,直到队列中有元素被取出
// arrayBlockingQueue.put("D");
System.out.println(arrayBlockingQueue.take());
System.out.println(arrayBlockingQueue.take());
System.out.println(arrayBlockingQueue.take());
// 队列为空之后再进行取出会一直阻塞,直到队列中放入新的元素
System.out.println(arrayBlockingQueue.take());
}
/**
* 队列的使用:不抛出异常
* 添加如果为队列已满则根据设置的超时时间,如果超时则返回 false
* 移除如果队列为空则则根据设置的超时时间,如果超时则返回 null
*/
public static void test4() throws InterruptedException {
ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(3);
System.out.println(arrayBlockingQueue.offer("A"));
System.out.println(arrayBlockingQueue.offer("B"));
System.out.println(arrayBlockingQueue.offer("C"));
// 添加如果为队列已满则根据设置的超时时间,如果超时则返回 false
// System.out.println(arrayBlockingQueue.offer("D",2, TimeUnit.SECONDS));
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
System.out.println(arrayBlockingQueue.poll());
// 移除如果队列为空则则根据设置的超时时间,如果超时则返回 null
System.out.println(arrayBlockingQueue.poll(2,TimeUnit.SECONDS));
}
}
同步队列
SynchronousQueue同步队列
同步队列跟阻塞队列不一样,同步队列不存储元素
put进去一个值之后必须take出来一个值,否则后续的put将一直阻塞
下面是jdk官方的解释
同步队列的使用方法
package com.gu.bq;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
/**
* 同步队列 SynchronousQueue
* 同步队列跟阻塞队列不一样,同步队列不存储元素
* put进去一个值之后必须take出来一个值,否则后续的put将一直阻塞
*/
public class SynchronousQueueDemo {
public static void main(String[] args) {
SynchronousQueue<String> synchronousQueue = new SynchronousQueue<>();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+ "put 1");
synchronousQueue.put("1");
System.out.println(Thread.currentThread().getName()+ "put 2");
synchronousQueue.put("2");
System.out.println(Thread.currentThread().getName()+ "put 3");
synchronousQueue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "->" + synchronousQueue.take());
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "->" + synchronousQueue.take());
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "->" + synchronousQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"B").start();
}
}
线程池
线程池:三大方法,7大参数,4中拒绝策略
池化技术
程序的运行,本质就是:占用系统的资源,创建,销毁十分浪费资源
线程池,连接池,内存池,对象池等等
池化技术就是优化资源的使用
池化技术:事先准备好一些资源,有人要用就拿,用完之后还回来
线程池的优点
降低资源的消耗
提高响应的速度
方便管理
线程可以复用,可以控制最大并发数,管理线程
Executors工具类-三大方法
package com.gu.pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Executors: 工具类、三大方法
* 使用线程池之后要使用线程池创建线程
*/
public class Demo01 {
public static void main(String[] args) {
// 单个线程
// ExecutorService executorService = Executors.newSingleThreadExecutor();
// 创建一个固定大小的线程池
// ExecutorService executorService = Executors.newFixedThreadPool(7);
// 可伸缩的线程池
ExecutorService executorService = Executors.newCachedThreadPool();
try {
for (int i = 1; i < 50; i++) {
// 使用线程池的方式创建线程
executorService.execute(()->{
System.out.println(Thread.currentThread().getName() + " -> OK");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
// 线程池用完,程序结束,关闭线程池
executorService.shutdown();
}
}
}
线程池七大参数及四大拒绝策略
源码分析
// 单个线程
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
// 创建一个固定大小的线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// 可伸缩的线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
// 线程池的七大参数
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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
图解线程池的工作方式:
package com.gu.pool;
import java.util.concurrent.*;
/**
* 线程池四大拒绝策略
* 线程池最大容纳数量 = 最大线程数量 + 阻塞队列长度
* new ThreadPoolExecutor.AbortPolicy()); 如果线程池已经满了,那么就不再接受处理,会抛出异常
* new ThreadPoolExecutor.CallerRunsPolicy()); 如果线程池已经满了,那么就不再接受处理,不会抛出异常,哪里来回那里去,返回到主线程main进行执行
* new ThreadPoolExecutor.DiscardPolicy()); 如果线程池已经满了,那么就不再接受处理,不会抛出异常,超过的请求会被直接舍弃
* new ThreadPoolExecutor.DiscardOldestPolicy()); 如果线程池已经满了,那么就不再接受处理,不会抛出异常,超过的请求会尝试与最早的竞争,成功则执行,失败会被丢弃
*/
public class Demo02 {
public static void main(String[] args) {
/**
* ThreadPoolExecutor 创建线程池
*/
ExecutorService executorService = new ThreadPoolExecutor(
3,
6,
5,
TimeUnit.SECONDS,
new ArrayBlockingQueue(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()); // 拒绝策略
try {
for (int i = 1; i < 50; i++) {
// 使用线程池的方式创建线程
executorService.execute(()->{
System.out.println(Thread.currentThread().getName() + " -> OK");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
// 线程池用完,程序结束,关闭线程池
executorService.shutdown();
}
}
}
为什么使用ThreadPoolExecutor创建线程池?
最大线程到底该如何定义?
CPU密集型与IO密集型
CPU密集型:几核的CPU定义为几,可以保持CPU效率最高
IO密集型:程序有15个大型任务,io十分占用资源,判断你的程序中十分耗IO的线程,一般情况下设置为大型IO任务的两倍
/**
* ThreadPoolExecutor 创建线程池
*/
ExecutorService executorService = new ThreadPoolExecutor(
3,
Runtime.getRuntime().availableProcessors(), // 获取CPU的核心数量
5,
TimeUnit.SECONDS,
new ArrayBlockingQueue(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()); // 拒绝策略
四大函数式接口
必须掌握
lambda表达式,函数式接口,链式编程,Stream流计算
四大函数式接口
Consumer,Function,Predicate,Supplier都是function包下的
函数型接口
Function,源码如下图所示
具体使用如下图所示:
package com.gu.function;
import java.util.function.Function;
/**
* Function 函数型接口
* 只要是 函数式接口 都可以用lambda简化
*/
public class Demo01 {
public static void main(String[] args) {
// 完整模式函数型接口
Function<String, String> function = new Function<String, String>() {
@Override
public String apply(String str) {
return str;
}
};
// 简化版函数型接口
Function<String, String> function1 = (str)->{
return str;
};
System.out.println(function.apply("完整版"));
System.out.println(function1.apply("简化版"));
}
}
断定式接口
Predicate,源码如下图所示
具体使用如下图所示:
package com.gu.function;
import java.util.function.Predicate;
/**
* Predicate 断定式接口
* 只要是 函数式接口 都可以用lambda简化
*/
public class Demo02 {
public static void main(String[] args) {
// 完整模式断定式接口
Predicate<String> predicate = new Predicate<String>() {
@Override
public boolean test(String str) {
return str.isEmpty();
}
};
// 简化版函数型接口
Predicate<String> predicate1 = (str)->{return str.isEmpty();};
System.out.println(predicate.test("完整版"));
System.out.println(predicate1.test(""));
}
}
供给型接口
Consumer,源码如下图所示
具体使用如下图所示:
package com.gu.function;
import java.util.function.Consumer;
/**
* Consumer 供给型接口:只有参数,没有返回值
* 只要是 函数式接口 都可以用lambda简化
*/
public class Demo03 {
public static void main(String[] args) {
// 完整模式供给型接口
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String str) {
System.out.println(str);
}
};
// 简化模式供给型接口
Consumer<String> consumer1 = (str)->{
System.out.println(str);
};
consumer.accept("完整版");
consumer1.accept("简化版");
}
}
消费型接口
Predicate,源码如下图所示
具体使用如下图所示:
package com.gu.function;
import java.util.function.Supplier;
/**
* Predicate 消费型接口:只有返回值,没有参数
* 只要是 函数式接口 都可以用lambda简化
*/
public class Demo04 {
public static void main(String[] args) {
// 完整模式消费型接口
Supplier<String> supplier = new Supplier<String>() {
@Override
public String get() {
return "完整版";
}
};
// 简化模式消费型接口
Supplier<String> supplier1 = ()->{ return "简化版"; };
System.out.println(supplier.get());
System.out.println(supplier1.get());
}
}
Stream流式计算
什么是流式计算?
大数据:存储+计算
集合、Mysql 本质就是存储;
计算都应该交给流来操作
Stream流式计算实例
package com.gu.stream;
import com.gu.pojo.User;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Stream;
/**
* Stream流式计算
*
* 题目要求:1分钟内完成,只能用一行代码
* 现在有十个用户,筛选:
* 1 ID必须是偶数
* 2 年龄必须大于20
* 3 用户名转换为大写
* 4 用户名字母倒着排序
* 5 只输出一个用户
*/
public class Demo01 {
public static void main(String[] args) {
User u1 = new User(1, "a", 21);
User u2 = new User(2, "b", 22);
User u3 = new User(3, "c", 23);
User u4 = new User(4, "d", 24);
User u5 = new User(5, "e", 25);
User u6 = new User(6, "f", 26);
User u7 = new User(7, "g", 27);
User u8 = new User(8, "h", 28);
User u9 = new User(9, "i", 29);
User u10 = new User(10, "j", 30);
// 首先转换为list,集合就是存储
List<User> userList = Arrays.asList(u1, u2, u3, u4, u5,u6,u7,u8,u9,u10);
/**
* 计算交给Stream流
*
* 该段代码使用了lambda表达式,链式编程,函数式接口,stream流计算
*/
userList.stream()
// 筛选
.filter(user -> { return user.getAge() % 2 == 0; })
.filter(user -> {return user.getAge() > 20;})
// 这里是一个Function接口,传入一个参数返回一个参数,这里我们将变为大写的字母返回
.map(user -> {return user.getName().toUpperCase();})
// 这里是Comparator.reverseOrder(),用来强制倒序,与.sorted((uu1,uu2)->{ return uu2.compareTo(uu1); })作用相同
.sorted(Comparator.reverseOrder())
// 分页,返回限制为1条
.limit(1)
.forEach(System.out::println);
}
}
还有Intstream,Doublestream,longstream
ForkJoin及Stream并行流
什么是ForkJoin?
出现与jdk1.7,并行执行任务,从而提升在超大数据量下的效率
与大数据中的Map Reduce(把大任务拆分为小任务执行):
Forkjoin的特点:工作窃取
当A线程执行完毕后会去继续执行B线程未完成的任务,因为ForkJoin中维护的是双端队列,所以可以另一个线程从另一端窃取任务继续执行
如何使用ForkJoin
Forkjoin与Stream并行流的实现
package com.gu.forkjoin;
import java.util.concurrent.RecursiveTask;
/**
* 求和计算的任务
* ForkJoin
* 如何使用forkjoin:
* 1,ForkjoinPool通过他来执行
* 2,计算任务forkjoinPool.execute(ForkJoinTask<?> task)
* 3,计算类要继承ForkJoinTask或他的具体实现类
* Stream并行流
*/
public class ForkJoinDemo extends RecursiveTask<Long> {
private Long start;
private Long end;
//临界值
private Long temp = 10000L;
public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}
/**
* 计算的方法
* @return 返回值为泛型
*/
@Override
protected Long compute() {
if ((end-start)<temp){
Long sum = 0L;
for (Long i = start; i <= end; i++) {
sum += i;
}
return sum;
}else{
//ForkJoin 递归
Long middle = (start + end) / 2;
ForkJoinDemo forkJoinDemo1 = new ForkJoinDemo(start,middle);
forkJoinDemo1.fork();// 拆分任务,把任务压入线程队列
ForkJoinDemo forkJoinDemo2 = new ForkJoinDemo(middle+1,end);
forkJoinDemo2.fork();// 拆分任务,把任务压入线程队列
return forkJoinDemo1.join()+forkJoinDemo2.join();
}
}
}
package com.gu.forkjoin;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
test3();
}
public static void test1(){
long start = System.currentTimeMillis();
long sum = 0L;
for (long i = 1L; i <= 100_000_0000L; i++) {
sum += i;
}
long end = System.currentTimeMillis();
System.out.println("sum="+ sum +" 时间为:"+(end-start));
}
public static void test2() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> forkJoinTask = new ForkJoinDemo(0L,100_000_0000L);
ForkJoinTask<Long> longForkJoinTask = forkJoinPool.submit(forkJoinTask);
long sum = longForkJoinTask.get();
long end = System.currentTimeMillis();
System.out.println("sum="+ sum +" 时间为:"+(end-start));
}
public static void test3(){
long start = System.currentTimeMillis();
long sum = LongStream.rangeClosed(0L, 100_000_0000L).parallel().reduce(0, Long::sum);
long end = System.currentTimeMillis();
System.out.println("sum="+ sum +" 时间为:"+(end-start));
}
}
异步回调
Future设计的初衷:对将来的某个事件进行建模
下面是jdk官方的解释,CompletableFuture是Future的实现类
Furure实现类CompletableFuture的使用方法
package com.gu.future;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
/**
* 异步回调
* 步骤:
* 异步执行
* 成功回调
* 失败回调
*/
public class Demo01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 发起一个请求 runAsync 无返回值 异步回调
/**
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("这个请求很不错");
});
System.out.println("11111111111111");
completableFuture.get();
*/
// 发起一个请求 supplyAsync 有返回值 异步回调
CompletableFuture<String> stringCompletableFuture = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName() + "=>supplyAsync=>String");
int i = 10/0;
return "200";
});
stringCompletableFuture.whenComplete((t,u)->{
// 正常的返回结果
System.out.println("t=>" + t);
// 错误的返回结果 java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
System.out.println("U=>" + u);
}).exceptionally((e)->{
// 可以获取到错误的返回结果
System.out.println(e.getMessage());
return "500";
}).get();
}
}
JMM
什么是JMM?
jmm是java内存模型,是不存在的东西,是一种概念,一种约定
关于JMM的一些约定
1、线程解锁前,必须把自己的共享变量,必须立刻刷新回主存
2、线程加锁前,必须读取主存中的最新值到自己的工作内存中
3、加锁和解锁必须是同一把锁
JMM就是线程工作内存与主存的一些约定规范
内存交互操作
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
JMM对这八种操作规则和对volatile的一些特殊规则就能确定哪里操作是线程安全,哪些操作是线程不安全的了。但是这些规则实在复杂,很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候,使用java的happen-before规则来进行分析。
Volatile
Volatile的理解
Volatile是java虚拟机提供的轻量级同步机制
1、保证可见性
2、不保证原子性
3、禁止指令重排
volatile的特性
- 可见性: 对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入
- 原子性: 对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性
volatile保证可见性
package com.gu.tvolatile;
import java.util.concurrent.TimeUnit;
/**
* volatile的可见性场景
*/
public class JMMDemo {
// 不加volatile,main线程将num变为1,但线程A不知道它改变了,线程就会一直卡在线程A的while循环
// public static int num = 0;
// 加上volatile,main线程将num变为1,会立即将最新值刷新给A线程,while在num值改变之后立即退出
public static volatile int num = 0;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (num == 0){
}
},"A").start();
TimeUnit.SECONDS.sleep(2);
num = 1;
System.out.println(num);
}
}
volatile不保证原子性
package com.gu.tvolatile;
/**
* volatile不保证原子性
*/
public class JMMDemo2 {
// 并不保证原子性
public volatile static int num = 0;
public static void add(){
// ++操作不是原子性操作,在编译后的class源码中,++分为三步:获取num,执行加1,对num重新赋值
num++;
}
public static void main(String[] args) {
// 理论上num为20000
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2)
{
Thread.yield();
}
System.out.println(num);
}
}
如果不加lock和synchronized如何保证num++原子性?
java.util.concurrent.atomic提供了一些原子包装类
package com.gu.tvolatile;
import java.util.concurrent.atomic.AtomicInteger;
/**
* AtomicInteger保证原子性
*/
public class JMMDemo2 {
// 并不保证原子性
// public volatile static int num = 0;
// AtomicInteger保证原子性
public volatile static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void add(){
// ++操作不是原子性操作,在编译后的class源码中,++分为三步:获取num,执行加1,对num重新赋值
// num++;
atomicInteger.getAndIncrement();
}
public static void main(String[] args) {
// 理论上num为20000
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2)
{
Thread.yield();
}
System.out.println(atomicInteger);
}
}
atomic的原子包装类实现的num++操作是直接调用的native方法,也并不是执行的+1操作,而是使用的CAS
指令重排
什么是指令重排
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型:
1,编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2,指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3,内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
1属于编译器重排序,2和3属于处理器重排序。从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序:
处理器在进行指令重排的时候会考虑数据之间的依赖性:as-if-serial 语义
as-if-serial 语义
as-if-serial的意思是:不管指令怎么重排序,在单线程下执行结果不能被改变。
不管是编译器级别还是处理器级别的重排序都必须遵循as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序。但是as-if-serial规则允许对有控制依赖关系的指令做重排序,因为在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果,但是多线程下确有可能会改变结果。
数据依赖-示例:
int a = 1; // 1
int b = 2; // 2
int c = a + b; // 3
上述代码,a和b不存在依赖关系,所以1、2可以进行重排序;
c依赖 a和b,所以3必须在1、2的后面执行。
控制依赖-示例:
public void use(boolean flag, int a, int b) {
if (flag) { // 1
int i = a * b; // 2
}
}
flag 和 i 存在控制依赖关系。
当指令重排序后,2这一步会将结果值写入重排序缓冲(Reorder Buffer,ROB)的硬件缓存中,当判断为true时,再把结果值写入变量i中。
happens-before 语义
JSR-133使用happens-before的概念来阐述操作之间的内存可见性。**在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。**这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。
两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个 操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一 个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。
happens-before 部分规则
程序顺序规则: 一个线程中的每个操作,happens-before于该线程中的任意后续操作。
主要含义是:在一个线程内不管指令怎么重排序,程序运行的结果都不会发生改变。和as-if-serial 比较像。
监视器锁规则: 对一个锁的解锁,happens-before于随后对这个锁的加锁。
主要含义是:同一个锁的解锁一定发生在加锁之后
管程锁定规则: 一个线程获取到锁后,它能看到前一个获取到锁的线程所有的操作结果。
主要含义是:无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
volatile变量规则: 对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
主要含义是:如果一个线程先去写一个volatile变量,然后另一个线程又去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
传递性: 如果A happens-before B,且B happens-before C,那么A happens-before C。
start()规则: 如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
主要含义是:线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
join()规则: 如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
主要含义是:如果在线程A执行过程中调用了线程B的join方法,那么当B执行完成后,在线程B中所有操作结果对线程A可见。
线程中断规则: 对线程interrupt方法的调用happens-before于被中断线程的代码检测到中断事件的发生。
主要含义是:响应中断一定发生在发起中断之后。
对象终结规则: 就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。
一个happens-before规则对应于一个或多个编译器和处理器重排序规则。
内存屏障
如何禁止指令重排,或者说如何解决指令重排问题:一种是使用内存屏障(volatile),另一种使用临界区(synchronized )。
如果我们使用内存屏障,那么JMM的处理器,会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为 Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。
内存屏障的类型
StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果。现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)。
那么上面的问题,我们可以在flag
处插入一个内存屏障,其作用是:保证在init()
方法中,第1步操作一定在第2步之前,禁止第1步和第2步操作出现指令重排序,代码如下:
public class ControlDep {
int a = 0;
volatile boolean flag = false;
public void init() {
a = 1; // 1
flag = true; // 2
//.......
}
public void use() {
if (flag) { // 3
int i = a * a; // 4
}
//.......
}
}
A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。也就是说程序执行执行完第2步的时候,处理器会将第2步和其之前的所有结果强制刷新到主内存。也就是说a=1也会被强制刷新到主内存中。那么当另一个线程执行到步骤3的时候,如果判断到flag=true时,那么第4步处a一定是等于1的,这样就保证了程序的正确运行。
顺序一致性
顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性:
1,一个线程中的所有操作必须按照程序的顺序来执行。
2,(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。
JMM对正确同步的多线程程序的内存一致性做了如下保证:
如果程序是正确同步的,程序的执行将具有顺序一致性(Sequentially Consistent)——即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。
我们看到JMM仅仅是保证了程序运行的结果是和顺序执行是一致,并没有实现真正的顺一致性。它又是怎么实现的呢?JMM使用了临界区(加锁)来保证程序的顺序执行,但是在临界区内是允许出现指令重排的(JMM不允许临界区内的代码“逸出”到临界区之外,那样会破坏监视器的语义)。
我们在回过来看下上面遇到的并发问题,在上面我们说了使用内存屏障来解决,这里我们使用临界区。
public class ControlDep {
int a = 0;
boolean flag = false;
public synchronized void init() {
a = 1; // 1
flag = true; // 2
//.......
}
public synchronized void use() {
if (flag) { // 3
int i = a * a; // 4
}
//.......
}
}
虽然线程A执行init()
方法时,在临界区内做了重排序,但由于监视器互斥执行的特性,线程B执行use()
方法时,根本无法“观察”到线程A在临界区内的重排序。这种重排序既提高了执行效率,又没有改变程序的执行结果。
JMM在具体实现上的基本方针为:在不改变(正确同步的)程序执 行结果的前提下,尽可能地为编译器和处理器的优化打开方便之门。
volatile写和读的内存语义
volatile写的内存语义: 当写一个volatile变量时,JMM会把该线程对应的本地内存中的所有共享变量值刷新到主内存
volatile读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取所有共享变量。
volatile内存语义的实现
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序
限制规则如下:
1,当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile写之前的操作不会被编译器重排序到volatile写之后。
2,当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保 volatile读之后的操作不会被编译器重排序到volatile读之前。
3,当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
volatile具体插入的内存屏障
在每个volatile写操作的前面插入一个StoreStore屏障。在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。在每个volatile读操作的后面插入一个LoadStore屏障。
锁的内存语义
1,当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A 对共享变量所做修改的)消息。
2,当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共 享变量所做修改的)消息。
线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。
final的内存语义
1,在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。也就是说只有将对象实例化完成后,才能将对象引用赋值给变量。
2,初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。也就是下面示例的4和5不能重排序。
3,当final域为引用类型时,在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
下面通过代码在说明一下:
public class FinalExample {
int i; // 普通变量
final int j; // final变量
static FinalExample obj;
public FinalExample() { // 构造函数
i = 1;// 写普通域
j = 2;// 写final域
}
public static void writer() { // 写线程A执行
// 这一步实际上有三个指令,如下:
// memory = allocate(); // 1:分配对象的内存空间
// ctorInstance(memory); // 2:初始化对象
// instance = memory; // 3:设置instance指向刚分配的内存地址
obj = new FinalExample();
}
public static void reader() { // 读线程B执行
FinalExample object = obj; // 4. 读对象引用
int a = object.i; // 5. 读普通域
int b = object.j; // 读final域
}
}
1,如果没有final语义的保证,在writer()方法中,那三个指令可能发生重排序,导致步骤3先于2执行,然后线程B在执行reader()方法时拿到一个没有初始化的对象。
2,在读一个对象的final域之前,一定会先读包含这个final 域的对象的引用。在这个示例程序中,如果该引用不为null,那么引用对象的final域一定已经 被A线程初始化过了。
final语义在处理器中的实现
-
会要求编译器在final域的写之后,构造函数return之前插入一个StoreStore障屏。
-
读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障。
单例模式
饿汉式单例模式
package com.gu.single;
/**
* 饿汉式单例模式的简单实现
*/
public class Hungry {
private Hungry(){
}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
懒汉式单例模式
package com.gu.single;
/**
* 懒汉式单例
*/
public class LazyMan {
private LazyMan(){
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
return new LazyMan();
}
}
静态内部类懒汉式单例
package com.gu.single;
/**
* 静态内部类实现单例模式
*/
public class HolderLazyMan {
private HolderLazyMan(){}
public static HolderLazyMan getInstance(){
return Holder.holderLazyMan;
}
static class Holder{
private static HolderLazyMan holderLazyMan = new HolderLazyMan();
}
}
DCL懒汉式单例
package com.gu.single;
import java.lang.reflect.Constructor;
/**
* DCL懒汉式单例(双重校验锁:Double Check Lock)
*/
public class DCLLazyMan {
private DCLLazyMan(){ }
/**
* 加volatile保证这个对象不会被重排序
*/
private volatile static DCLLazyMan dclLazyMan;
public static DCLLazyMan getInstance(){
/**
* 判断DCLLazyMan是否为空
* 如果第一次为空,锁当前class
* 第二次为空,返回new的当前对象
*
* 如果都不为空,直接return
*/
if (null == dclLazyMan){
/**
* 只加synchronized(不加volatile)会有问题的场景
* new DCLLazyMan();底层(class中)具体实现步骤是:
* 1,分配内存空间
* 2,执行构造方法,初始化对象
* 3,把这个对象指向这个空间
*
* 在多线程场景下,A线程在执行new DCLLazyMan();时,这三步有可能被重排序,执行顺序打乱为132,而线程B在线程A执行到3的时候还是可以执行new DCLLazyMan();
*
* 这里解释一下为什么synchronized也可以禁止重排序但是这里还是会重新排序:
* synchronized实现的禁止重排序是将它包含的代码放入synchronized缓冲区,这个缓冲区的代码不能和其他位置代码进行重排,但这个缓冲区内部还是可以进行重排序
*/
synchronized (DCLLazyMan.class){
if (null == dclLazyMan) {
return new DCLLazyMan();
}
}
}
return dclLazyMan;
}
}
class Test1{
public static void main(String[] args) throws Exception {
/**
* 正常使用
*/
DCLLazyMan dclLazyMan = DCLLazyMan.getInstance();
System.out.println(dclLazyMan);
/**
* 通过反射破坏单例模式,除了通过枚举实现单例模式其他都会被反射破解
*/
Constructor<DCLLazyMan> constructor = DCLLazyMan.class.getDeclaredConstructor();
// 这里置为空,可以通过newInstance获取到实例化对象
constructor.setAccessible(true);
DCLLazyMan dclLazyMan1 = constructor.newInstance();
System.out.println(dclLazyMan1);
}
}
枚举单例模式
package com.gu.single;
import java.lang.reflect.Constructor;
/**
* 枚举实现单例模式
*/
public enum EnumLazyMan {
INSTANCE;
public EnumLazyMan getInstance(){
return INSTANCE;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class Test{
public static void main(String[] args) throws Exception {
/**
* 正常使用
*/
EnumLazyMan enumLazyMan1 = EnumLazyMan.INSTANCE;
enumLazyMan1.setName("ssssss");
System.out.println(enumLazyMan1.getName());
/**
* 通过反射破解enum单例模式,无法破解,提示:Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
*
* 枚举实现单例模式没有空的构造方法,默认会生成String.class,int.class的构造方法
* 但即使破解了,在进行newInstance的时候,其内部会判断如果对象类型是枚举:Cannot reflectively create enum objects,无法反射式创建枚举对象
*/
Constructor<EnumLazyMan> declaredConstructor = EnumLazyMan.class.getDeclaredConstructor(String.class,int.class);
// 这里置为空,可以通过newInstance获取到实例化对象
declaredConstructor.setAccessible(true);
EnumLazyMan enumLazyMan2 = declaredConstructor.newInstance();
System.out.println(enumLazyMan2.getName());
}
}
单例模式汇总
饿汉式单例模式因为是在系统启动时,无论是否使用就加载,会有性能问题,所以并不推荐使用
普通懒汉式单例模式在单线程下使用没有任何问题,但在多线程情况下无法保证单例
静态内部懒汉式单例在单线程下使用没有任何问题,但在多线程情况下无法保证单例
DCL懒汉式单例在单线程,多线程情况下并不会出现问题,但在某些极端场景:通过反射获取类对象时,单例还是会被破坏
枚举单例模式在单线程,多线程,以及极端情况使用反射获取类对象是都可以很好的保证单例,在使用反射获取枚举类对象时,其内部判断了如果时枚举类型则禁止反射进行实例化,而声明的枚举实例其实对应一个用 static final修饰的变量,其初始化在静态块中完成。所以本质上线程安全也是通过类加载过程中,类构造器(< clinit >)的调用实现了同步。枚举里的值编译后确实都相当于final static修饰的变量,和饿汉很相似,但他实际暴露出去的单例变量却不是在启动时就初始化的(你可以debug感受一下),这一点和懒汉很像
CAS(CompareAndSet)
比较并交换
Java中的CAS实现
package com.gu.cas;
import java.util.concurrent.atomic.AtomicInteger;
/**
* java中的 比较并交换 CAS compareAndSet
*/
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
/**
* expect 期望值 update 更新值
* public final boolean compareAndSet(int expect, int update) {
* return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
* }
* atomicInteger.compareAndSet(2020, 2021) 的意思:
* 如果atomicInteger的值时2020,那么将其更新为2021
*/
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
/**
* 将atomicInteger进行+1
*
*/
atomicInteger.getAndIncrement();
System.out.println(atomicInteger.get());
}
}
根据源码解释getAndIncrement的+1操作
//AtomicInteger类
//变量value; 相当于i++中的i,也就是AtomicInteger的当前值
private volatile int value;
//创建Unsafe类的实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
//变量value的偏移量, 具体赋值是在下面的静态代码块中中进行的
private static final long valueOffset;
//在静态代码块中获取变量value的偏移量
static {
try {
//获取value变量的偏移量, 赋值给valueOffset
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//执行value++操作
public final int getAndIncrement() {
//this是当前对象, valueOffset是变量value的偏移量,1是固定值1
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//Unsafe类
//获取内存地址为obj+offset的变量值, 并将该变量值加上delta
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//通过对象和偏移量获取变量的值, 由于getIntVolatile获取的值, 所有线程看到的var5都是一样的
var5 = this.getIntVolatile(var1, var2);
/*
while中的compareAndSwapInt()方法尝试修改v的值,具体地, 该方法也会通过var1和var2获取变量的值
如果这个值和var5不一样, 说明其他线程修改了var1+var2地址处的值, 此时compareAndSwapInt()返回false, 继续循环
如果这个值和var5一样, 说明没有其他线程修改var1+var2地址处的值, 此时可以将var1+var2地址处的值改为var5+var4, compareAndSwapInt()返回true, 退出循环
Unsafe类中的compareAndSwapInt()方法是原子操作, 所以compareAndSwapInt()修改var1+var2地址处的值的时候不会被其他线程中断
*/
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
}
getAndAddInt()就是自旋锁
Unsafe类
Unsafe的主要功能
上面讲到就是CAS相关的操作以及volatile的读写操作
Unsafe的特点
Java和C++语言的一个重要区别就是Java中我们无法直接操作一块内存区域,不能像C++中那样可以自己申请内存和释放内存。
Java中的Unsafe类为我们提供了类似C++手动管理内存的能力。
Unsafe类,全名是sun.misc.Unsafe
,这个类非常危险,一般开发者不会用到这个类。
Unsafe类是final,不允许继承。且构造函数是private的:
public final class Unsafe {
private static final Unsafe theUnsafe;
public static final int INVALID_FIELD_OFFSET = -1;
private static native void registerNatives();
// 构造函数是private的,不允许外部实例化
private Unsafe() {
}
...
}
因此无法在外部对Unsafe进行实例化。
获取Unsafe
Unsafe无法实例化,但是我们可以通过反射来获取Unsafe:
public Unsafe getUnsafe() throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
return unsafe;
}
如果需要看Unsafe的相关方法可以查阅jdk官方文档
CAS的优缺点:
cas优点:在并发量不是很高时cas机制会提高效率。
cas缺点:
1、cpu开销大,在高并发下,许多线程,更新一变量,多次更新不成功,循环反复,给cpu带来大量压力。
2、一次只能保证一个变量的原子性,只是一个变量的原子性操作,并不能保证代码块的原子性。
3、ABA问题
CAS的ABA问题
示例:小胡桌子上有一块蛋糕,小胡在睡觉,2分钟后小谷看到了蛋糕给吃掉了,但是怕被骂就又去买了一块,这就是ABA问题
实际上小胡吃到的已经不是当初买的那块,并且小谷多吃了一块蛋糕
代码:
package com.gu.cas;
import java.util.concurrent.atomic.AtomicInteger;
/**
* CAS 的ABA问题
*/
public class ABADemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
/**
* 偷吃的小谷
*/
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
// 吃完之后赶紧去买了新的
System.out.println(atomicInteger.compareAndSet(2021, 2020));
System.out.println(atomicInteger.get());
/**
* 睡觉的小胡 并不知道小谷偷吃了
*/
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
}
}
使用原子引用解决ABA问题
package com.gu.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* CAS的ABA问题通过原子引用解决
*/
public class ABADemo02 {
public static void main(String[] args) {
// initialRef 值 initialStamp 版本号
// 这里通常是使用USER对象来判断的,使用Integer会出现问题,具体问题看文章描述
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(20,1);
/**
* 这里是小谷看到蛋糕,冲过去没看产品编号就吃了,然后赶紧又去买了一份蛋糕回来,他以为小胡不会发现
*/
new Thread(()->{
// 获得版本号
int stamp = atomicStampedReference.getStamp();
System.out.println("A1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 这里的操作时如果值是20,然后拿到最新版本号,将值改为21,并且把版本号+1
*/
System.out.println("A2=>" + atomicStampedReference.compareAndSet(
20,
21,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp()+1));
System.out.println("A2=>" + atomicStampedReference.getStamp());
System.out.println("A3=>" + atomicStampedReference.compareAndSet(
21,
20,
atomicStampedReference.getStamp(),
atomicStampedReference.getStamp()+1));
System.out.println("A3=>" + atomicStampedReference.getStamp());
},"A").start();
/**
* 这里是小胡买了蛋糕,但是小胡留了个心眼把产品编码拍照留下来了,在吃的时候对比了一下发现不是原来买的蛋糕了
*/
new Thread(()->{
// 获得版本号
int stamp = atomicStampedReference.getStamp();
System.out.println("B1=>" + stamp);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B2=>" + atomicStampedReference.compareAndSet(
20,
66,
stamp,
stamp+1));
System.out.println("B2=>" + atomicStampedReference.getStamp());
},"B").start();
}
}
在阿里巴巴规约中说明:Integer在存储-128至127的值时,会将其存储在缓存中,并且复用已有对象,在这个区间内的值可以直接使用==判断,超过这个区间的所有数据都会在堆上生成新的对象,上述代码中使用Integer,如果传入值为2020,在执行后续代码是将无法通过CAS修改成功(详见阿里巴巴规约 OOP规约 第七条)
JAVA中多种锁的理解
公平锁、非公平锁
公平锁FairSync:非常公平,不能插队,必须先进先出
非公平锁NonfairSync:非常不公平,可以插队(默认都是非公平锁)
// 非公平锁
Lock lock = new ReentrantLock();
// 构造方法为:
public ReentrantLock() {
sync = new NonfairSync();
}
// 公平锁
Lock lock = new ReentrantLock(true);
// 如果构造函数传入true,则为公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
简单原理:在实现上,公平锁在进行lock时,首先会进行tryAcquire()操作。在tryAcquire中,会判断等待队列中是否已经有别的线程在等待了。如果队列中已经有别的线程了,则tryAcquire失败,则将自己加入队列。如果队列中没有别的线程,则进行获取锁的操作。
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
非公平锁,在进行lock时,会直接尝试进行加锁,如果成功,则获取到锁,如果失败,则进行和公平锁相同的动作。
从公平锁和非公平的实现上来看,他们的操作基本相同,唯一的区别在于,在lock时,非公平锁会直接先进行尝试加锁的操作。
当前一个线程完成了锁的使用,并且释放了,而且此时等待队列非空时,如果这是有新线程申请锁,那么,公平锁和非公平锁的表现就会出现差异。
上述只是简单的公平锁非公平锁,后续还需要继续研究
可重入锁
又名 递归锁
可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
示例:家里大门可以打开,卧室,卫生间,厨房就都可以进入
lock和synchronized都是可重入锁,详细使用其实在上面8锁的时候有简单提到,这里不再重复
可重入锁最大的作用就是避免死锁
测试一:Synchronized
/**
* 可重入锁(也叫递归锁)
* 指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码
* 在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
*/
public class ReentrantLockDemo {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
// T1 线程在外层获取锁时,也会自动获取里面的锁
new Thread(()->{
phone.sendSMS();
},"T1").start();
new Thread(()->{
phone.sendSMS();
},"T2").start();
}
}
class Phone{
public synchronized void sendSMS(){
System.out.println(Thread.currentThread().getName()+" sendSMS");
sendEmail();
}
public synchronized void sendEmail(){
System.out.println(Thread.currentThread().getName()+" sendEmail");
}
}
测试二:ReentrantLock
package com.gu;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 可重入锁(也叫递归锁)
* 指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码
* 在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
*/
public class ReentrantLockDemo {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
// T1 线程在外层获取锁时,也会自动获取里面的锁
new Thread(phone,"T1").start();
new Thread(phone,"T2").start();
}
}
class Phone implements Runnable{
Lock lock = new ReentrantLock();
@Override
public void run() {
get();
}
public void get(){
lock.lock();
// lock.lock(); 锁必须匹配,如果两个锁,只有一个解锁就会失败
try {
System.out.println(Thread.currentThread().getName()+" get()");
set();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
// lock.lock();
}
}
public void set(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+" set()");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
自旋锁
自旋锁(spinlock)
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下 文切换的消耗,缺点是循环会消耗CPU。
getAdnAddInt()就是自旋锁
测试代码:
package com.gu;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* 自旋锁,非重入自旋锁
*/
public class SpinLockDemo {
// 原子引用线程, 没写参数,引用类型默认为null
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//上锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"==>mylock");
// 自旋
while (!atomicReference.compareAndSet(null,thread)){
}
}
//解锁
public void myUnlock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName()+"==>myUnlock");
}
// 测试
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(()->{
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnlock();
},"T1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
spinLockDemo.myLock();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
spinLockDemo.myUnlock();
},"T2").start();
}
}
自旋锁还有一种可重入自旋锁,暂时没写
死锁
死锁是什么
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干 涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性 就很低,否者就会因为争夺有限的资源而陷入死锁。
产生死锁主要原因:
1、系统资源不足
2、进程运行推进的顺序不合适
3、资源分配不当
package com.gu;
import java.util.concurrent.TimeUnit;
/**
* 死锁
*/
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new HoldLockThread(lockA,lockB),"T1").start();
new Thread(new HoldLockThread(lockB,lockA),"T2").start();
}
}
class HoldLockThread implements Runnable{
private String lockA;
private String lockB;
public HoldLockThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
public void run() {
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+"lock:"+lockA+"=>get"+lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+"lock:"+lockB+"=>get"+lockA);
}
}
}
}
死锁排查
拓展java自带工具操作:
1、查看JDK目录的bin目录
2、使用 jps -l 命令定位进程号
3、使用 jstack 进程号 找到死锁查看