5、集合线程安全
-
集合的线程不安全演示
- ArrayList
package com.codetip.codejuc.juc.conllections;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class ThreadArrayList {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i <= 100; i++) {
new Thread(() -> {
// 向线程中添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
// 从里面取出内容
System.out.println(list); // 产生集合不安全的地方,取的时候,有线程还在放入内容,出现了并发修改问题
}, String.valueOf(i)).start();
}
}
}
运行结果如下: ?
-
解决方案:
- 使用vector(不经常使用-效率不高)
public class ThreadArrayListOnVector {
public static void main(String[] args) {
List<String> list = new Vector<>(); // 使用vector
for (int i = 0; i <= 100; i++) {
new Thread(() -> {
// 向线程中添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
// 从里面取出内容
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
- 使用Collections
public class ThreadArrayListOnCollections {
public static void main(String[] args) {
// 通过Collections工具包的工具类
List<String> list = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i <= 100; i++) {
new Thread(() -> {
// 向线程中添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
// 从里面取出内容
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
-
使用写时复制技术(JUC中的CopyOnWriteArrayList)
- 概述:多个线程读的时候读取同一个内容,写的时候复制之前一个相同的内容往里面写,写完之后在覆盖或合并之前的内容。代码如下:
public static void main(String[] args) {
// 使用Juc中的写时复制技术
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i <= 100; i++) {
new Thread(() -> {
// 向线程中添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
// 从里面取出内容
System.out.println(list);
}, String.valueOf(i)).start();
}
}
源码如下: // CopyOnWriteArrayList add方法源码
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();// 上锁
try {
Object[] elements = getArray();// 得到当前集合的数组
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1); // 复制一份新的
newElements[len] = e; // 写入新的内容
setArray(newElements); // (重新赋值)合并之前的内容
return true;
} finally {
lock.unlock(); // 解锁
}
}
-
HashSet
-
线程不安全演示 // 演示HashSet线程不安全问题
public static void main(String[] args) {
Set<String> list = new HashSet<>();
for (int i = 0; i <= 100; i++) {
new Thread(() -> {
// 向线程中添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
// 从里面取出内容
System.out.println(list);
}, String.valueOf(i)).start();
}
}
? -
解决方案
- 使用Juc中的CopyOnWriteArraySet解决
public static void main(String[] args) {
Set<String> list = new CopyOnWriteArraySet<>();
for (int i = 0; i <= 100; i++) {
new Thread(() -> {
// 向线程中添加内容
list.add(UUID.randomUUID().toString().substring(0, 8));
// 从里面取出内容
System.out.println(list);
}, String.valueOf(i)).start();
}
}
-
HashSet底层其实就是一个HashMap ,HashMap 的就是HashSet的值。HashMap的Key是不可重复,切无序的。 源码如下: private transient HashMap<E,Object> map;
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
-
HashMap
-
线程不安全演示 // HashMap线程不安全演示
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
for (int i = 0; i <= 100; i++) {
String key = String.valueOf(i);
new Thread(() -> {
// 向线程中添加内容
map.put(key, UUID.randomUUID().toString().substring(0, 8));
// 从里面取出内容
System.out.println(map);
}, String.valueOf(i)).start();
}
}
? -
解决方案,使用Juc包中的,ConcurrentHashMap public static void main(String[] args) {
Map<String, String> map = new ConcurrentHashMap<>(); // JUC 包中的ConcurrentHashMap
for (int i = 0; i <= 100; i++) {
String key = String.valueOf(i);
new Thread(() -> {
// 向线程中添加内容
map.put(key, UUID.randomUUID().toString().substring(0, 8));
// 从里面取出内容
System.out.println(map);
}, String.valueOf(i)).start();
}
}
6、多线程锁
-
八种锁演示 package com.codetip.codejuc.juc;
import java.util.concurrent.TimeUnit;
class Phone {
public synchronized void sendSms() throws InterruptedException {
// 2.停留四秒 先打印短信还是邮件
// 停留四秒钟
TimeUnit.SECONDS.sleep(4);
System.out.println("发送短信-----send_SMS");
}
public synchronized void sendEmail() {
System.out.println("发送邮件-----send_Email");
}
public static synchronized void sendSmsS() throws InterruptedException {
// 2.停留四秒 先打印短信还是邮件
// 停留四秒钟
TimeUnit.SECONDS.sleep(4);
System.out.println("发送短信-----send_SMS");
}
public static synchronized void sendEmailS() {
System.out.println("发送邮件-----send_Email");
}
public void getHello() {
System.out.println("-------getHello");
}
}
/**
* 8锁
* 1。一部手机,标准访问,先打印短信还是邮件 停留先时间注释掉,后面全部打开
* 发送短信-----send_SMS
* 发送邮件-----send_Email
*
* 2.一部手机,停留四秒 先打印短信还是邮件
* 发送短信-----send_SMS
* 发送邮件-----send_Email
* 原因分析:1和2。执行顺序,第一个sms执行方法上有synchronized方法已经锁住当前对象,Email等待获取锁
* synchronized 锁的是当前对象
*
* 3.新增普通的hello方法,是先打印短信还是hello
* -------getHello
* 发送短信-----send_SMS
*
* 原因:普通方法没有加锁,和锁没有关系,所以就直接执行了
*
* 4.现在有两部手机,先打印短信还是邮件
* 发送邮件-----send_Email
* 发送短信-----send_SMS (等了四秒钟)
* 原因分析:有两个对象,synchronized锁定只是当前对象,用的不是同一把锁,
*
* 5.两个静态同步方法 1部手机,先打印短信还是邮件
* 发送短信-----send_SMS
* 发送邮件-----send_Email
*
* 6.两个静态同步方法 2部手机,先打印短信还是邮件
* 发送短信-----send_SMS
* 发送邮件-----send_Email
*
* 原因分析 5和6 static synchronized 锁定的范围发送了变化 ,不是当前对象this ,而是当前类的Class对象(类的字节码对象)
*
* 7。一个静态同步方法,1个普通方法,1部手机,先打印短信还是邮件
* 发送邮件-----send_Email
* 发送短信-----send_SMS
*
* 8.一个静态同步方法,1个普通方法,2部手机,先打印短信还是邮件
* 发送邮件-----send_Email
* 发送短信-----send_SMS(停留四秒)
*
* 原因分析:7和8 还是锁的作用范围不一样, static synchronized 锁定的是 类的Class对象, 不加static 锁定的是当前对象this
*
*/
public class EightLock {
public static void main(String[] args) throws InterruptedException {
Phone phone1 = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
try {
// 1.标准访问,先打印短信还是邮件
// 2.停留四秒 先打印短信还是邮件
// 3.新增普通的hello方法,是先打印短信还是hello
//phone1.sendSms();
// 5.两个静态同步方法 1部手机,先打印短信还是邮件
// 6.两个静态同步方法 2部手机,先打印短信还是邮件
// 7.一个静态同步方法,1个普通方法,1部手机,先打印短信还是邮件
// 8.一个静态同步方法,1个普通方法,2部手机,先打印短信还是邮件
phone1.sendSmsS();
} catch (Exception e) {
e.printStackTrace();
}
}, "AA").start();
Thread.sleep(100);
new Thread(() -> {
try {
// 1.标准访问,先打印短信还是邮件
// 2.停留四秒 先打印短信还是邮件
// phone1.sendEmail();
// 3.新增普通的hello方法,是先打印短信还是hello
// phone1.getHello();
// 4.现在有两部手机,先打印短信还是邮件
// phone2.sendEmail();
// 5.两个静态同步方法 1部手机,先打印短信还是邮件
// phone1.sendEmailS();
// 6.两个静态同步方法 2部手机,先打印短信还是邮件
// phone2.sendEmailS();
// 7.一个静态同步方法,1个普通方法,1部手机,先打印短信还是邮件
// phone1.sendEmail();
// 8.一个静态同步方法,1个普通方法,2部手机,先打印短信还是邮件
phone2.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
}, "BB").start();
}
}
总结:synchronized实现同步的基础:Java中的每一个对象都可以作为锁,具体表现为一下三种情况
1、对于普通方法:锁的当前实例对象(通常说的This) |
---|
2、对于静态同步方法:锁的是当前类的Class对象 | 3、对于同步方法块,锁定的是synchronized括号里面的配置的对象 |
-
公平锁和非公平锁
-
具体代码如下 // 默认是非公平锁
private final Lock lock = new ReentrantLock();
// 源码如下
public ReentrantLock() {
sync = new NonfairSync();
}
// 如何使用公平锁 创建类的时候,在构造参数中传入 true 来指明使用公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// 源码如下
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1); // 尝试获取锁
}
/**
* 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;
}
}
-
特点:
- 非公平锁特点:其他线程被饿死,效率高。(可以参考卖票的例子)
- 公平锁特点:每个线程都有可能获得多,效率稍低一点。(可以参考卖票的例子)
-
可重入锁(递归锁)
- synchronized 隐式的可重入锁(上锁和解锁都是自动完成的)
- lock:显示的可重入锁
代码演示:synchronized // 案例一 同步代码块
Object o = new Object();
new Thread(() -> {
synchronized (o) {
System.out.println(Thread.currentThread().getName() + "外层");
synchronized (o) {
System.out.println(Thread.currentThread().getName() + "中层");
synchronized (o) {
System.out.println(Thread.currentThread().getName() + "内层");
}
}
}
}, "aa").start();
// 案例二 同步方法
public class ReSyn {
public synchronized void add() {
add();
}
// 使用synchronized关键字演示可重入锁
public static void main(String[] args) {
new ReLock().add();
}
}
// 案例二运行出现:内存溢出,原因:synchronized 是一个可重入锁,锁住的add方法,因为是同一个对象,所以还可以进入调用add方法(可重入锁(递归锁))
代码演示:Lock Lock lock = new ReentrantLock();
// 创建线程
new Thread(() -> {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "外层");
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "内层");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}, "aa").start();
}
演示锁不关闭: // 使用Lock关键字 锁不关闭
public static void main(String[] args) {
Lock lock = new ReentrantLock();
// 创建线程
new Thread(() -> {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "外层");
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "内层");
} finally {
// 如果里面的锁不关闭
// lock.unlock();
}
} finally {
lock.unlock();
}
}, "AA").start();
// 创建一个新的线程
new Thread(() -> {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "");
} finally {
lock.unlock();
}
}, "BB").start();
}
}
结果如下:出现死锁状态,原因:上面的所没有释放,下面的线程无法获取不到锁。 ? ? -
死锁两个或两个以上的线程在执行的过程,因为争夺资源而造成一种互相等待现象,如果没有哦外力的干涉,他没无法再执行下去。
-
产生死锁的原因:
- 系统资源不足
- 线程运行推进顺序不对
- 资源分配不当
-
代码演示: package com.codetip.codejuc.juc.deallock;
import java.util.concurrent.TimeUnit;
public class DealLock {
static Object a = new Object();
static Object b = new Object();
public static void main(String[] args) {
// 持有锁a 等待获取锁b
new Thread(() -> {
synchronized (a) {
System.out.println(Thread.currentThread().getName() + "持有锁a,试图获取锁b");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println(Thread.currentThread().getName() + "获取锁b");
}
}
}, "aa").start();
// 持有锁b,等待获取锁a
new Thread(() -> {
synchronized (b) {
System.out.println(Thread.currentThread().getName() + "持有锁b,试图获取锁a");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a) {
System.out.println(Thread.currentThread().getName() + "获取锁a");
}
}
}, "bb").start();
}
}
运行截图如下: ? ? -
验证死锁
-
命令:使用jps(类似Linux中的 ps -ef) -
jstack jvm中自带的堆栈跟踪命令 具体演示:
|