1、Local接口
LOCK是类,可通过类实现同步访问,多个接口实现类:可重入锁等
lock的编程步骤同synchronized
- 创建资源类,在资源类中创建属性和操作方法
- 创建多个线程,调用资源类的操作方法
可重入锁的代码定义
private final ReentrantLock lock = new ReentrantLock(true);
-
上锁:lock.lock();
-
解锁:lock.unlock();
上锁与解锁中的代码如果出现异常,解锁会执行不了,所以最好加
try..finally
代码:
@Slf4j
public class SaleTicket {
public static void main(String[] args) {
LTicket lTicket = new LTicket();
new Thread(()->{
for (int i = 0; i < 40; i++) {
lTicket.sale();
}
},"AA").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
lTicket.sale();
}
},"BB").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
lTicket.sale();
}
},"CC").start();
}
}
@Slf4j
class LTicket{
private int number = 30;
// 创建可重入锁
private final ReentrantLock lock = new ReentrantLock();
public void sale(){
// 上锁
lock.lock();
try {
if(number > 0){
System.out.println(Thread.currentThread().getName() + "卖出:" + number-- + "剩余:" + number);
}
} finally {
// 解锁
lock.unlock();
}
}
}
1.1 Lock和synchronized有以下几点不同
-
Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现。
-
synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unlock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
-
Lock可以让等待锁的线程响应中断,而synchronized却无法办到。
-
Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(既有大量线程同时竞争),此时Lock的性能要远远由于synchronized。
1.2 使用Lock和synchronized方式处理下面问题
(1)有两个线程,实现对一个初始值是0的变量,一个线程对值+1,另外一个线程对值-1的操作
-
使用synchrionzed方法
public class ThreadDemo01 { public static void main(String[] args) { share share = new share(); new Thread(()->{ for (int i = 1; i <= 10 ; i++) { try { share.incr(); } catch (InterruptedException e) { e.printStackTrace(); } } },"AA").start(); new Thread(()->{ for (int i = 1; i <= 10 ; i++) { try { share.decr(); } catch (InterruptedException e) { e.printStackTrace(); } } },"BB").start(); } } class share{ private int number = 0; // +1方法 public synchronized void incr() throws InterruptedException { if(number != 0){ //等待 this.wait(); } //如果number等于0,则加一 number++; System.out.println(Thread.currentThread().getName() + " : " + number); //在通知其他线程 this.notifyAll(); } // -1方法 public synchronized void decr() throws InterruptedException { // 判断number不等于1,则等待 if(number != 1){ this.wait(); } number--; System.out.println(Thread.currentThread().getName() + " : " + number); //在通知其他线程 this.notifyAll(); } }
打印结果:
结果是两个线程交替执行各10次
如果使用多个线程,添加额外两个线程,且操作要依次执行
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.incr(); //+1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.decr(); //-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"DD").start();
打印结果:
要么出现大于1的值,要么出现小于0的值。这种现象出现的原因就是因为主要是调用wait()方法出现虚假唤醒现象导致:如果一个线程执行完毕后,通知其他线程,该线程又进入等待睡眠,可能会因为某些原因被唤醒后,if结构的语句就不会判断了,一直往下执行,所以需要将if换成while结构,每次都判断。因为wait在哪里睡眠就在哪里被唤醒,结果被某个异常唤醒了后回不去了,if结构不会再判断了,需要更改为while,一般wait()方法使用在循环里面。
while(number != 0) { // 判断number值是否是0,如果不是0,等待
this.wait(); // 在哪里睡,就在哪里醒
}
打印结果:
2、线程间的定制化通信
2.1 需求如下图
所谓定制化通信,需要让线程进行一定的顺序操作。
案例:启动三个线程,按照如下要求:
AA打印5次,BB打印10次,CC打印15次,一共进行10轮
具体思路:
每个线程添加一个标志位,是该标志位则执行操作,并且修改为下一个标志位,通知下一个标志位的线程
具体代码:
//第一步 创建资源类
class ShareResource {
//定义标志位
private int flag = 1; // 1 AA 2 BB 3 CC
//创建Lock锁
private Lock lock = new ReentrantLock();
//创建三个condition
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
//打印5次,参数第几轮
public void print5(int loop) throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while(flag != 1) {
//等待
c1.await();
}
//干活
for (int i = 1; i <=5; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
}
//通知
flag = 2; //修改标志位 2
c2.signal(); //通知BB线程
}finally {
//释放锁
lock.unlock();
}
}
//打印10次,参数第几轮
public void print10(int loop) throws InterruptedException {
lock.lock();
try {
while(flag != 2) {
c2.await();
}
for (int i = 1; i <=10; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
}
//修改标志位
flag = 3;
//通知CC线程
c3.signal();
}finally {
lock.unlock();
}
}
//打印15次,参数第几轮
public void print15(int loop) throws InterruptedException {
lock.lock();
try {
while(flag != 3) {
c3.await();
}
for (int i = 1; i <=15; i++) {
System.out.println(Thread.currentThread().getName()+" :: "+i+" :轮数:"+loop);
}
//修改标志位
flag = 1;
//通知AA线程
c1.signal();
}finally {
lock.unlock();
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
shareResource.print5(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
shareResource.print10(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
shareResource.print15(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();
}
}
3、集合的线程安全
/**
* @author xxx
* @date 2022-03-01 10:53
* @description
* 线程不安全问题:
* List集合是线程不安全的
* 造成的原因:并发修改异常
*/
public class ThreadDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
// 向集合中添加数据
list.add(UUID.randomUUID().toString().substring(0,8));
// 从集合中取数据
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
打印结果:
查看源码,主要是这个添加的方法是因为该方法在列表中是不安全的,没有synchronized声明
boolean add(E e);
java.util.ConcurrentModificationException
为并发修改问题
3.1 Vector解决List线程不安全
通过list下的实现类Vector
因为在Vector下的add普遍都是线程安全
查看源码
public synchronized boolean add(E e) {
modCount++;
add(e, elementData, elementCount);
return true;
}
在改变代码时,只需要将其修改为List<String> list = new Vector<>();
但此方法用的比较少,因为在jdk 1.0的版本适用
3.2 Collections解决List线程不安全
Collections类中的很多方法都是static静态
其中有一个方法是返回指定列表支持的同步(线程安全的)列表为synchronizedList(List <T> list)
具体代码为
List<String> list = Collections.synchronizedList(new ArrayList<>());
此方法也比较古老,很少使用
3.3 CopyOnWriteArrayList解决List线程不安全
// CopyOnWriteArrayList解决线程安全性问题
List<String> list = new CopyOnWriteArrayList<>();
涉及的底层原理是写时复制技术
- 读得时候并发(多个线程操作),支持并发读
- 独立写,每次写都是先复制相同的空间到某个区域,将需要添加的内容写到新区域,内容写完之后,再新旧合并,再读取新区域的内容。这个方案的好处是即坚固了并发读,又照顾到了独立写。不会产生修改并发异常
3.4 HashSet集合线程不安全问题解决
/**
* 演示HashSet集合不安全问题
*/
Set<String> set = new HashSet<>();
for (int i = 0; i < 40; i++) {
new Thread(()->{
// 向集合中添加数据
set.add(UUID.randomUUID().toString().substring(0,8));
// 从集合中取数据
System.out.println(set);
},String.valueOf(i)).start();
}
打印结果:
解决HashSet集合线程不安全的问题是使用CopyOnWriteArraySet()方法
Set<String> set =new CopyOnWriteArraySet<>();
for (int i = 0; i < 40; i++) {
new Thread(()->{
// 向集合中添加数据
set.add(UUID.randomUUID().toString().substring(0,8));
// 从集合中取数据
System.out.println(set);
},String.valueOf(i)).start();
}
3.5 HashMap线程不安全处理方案
/**
* 演示HashMap线程不安全问题
*/
Map<String, String> hashMap = new HashMap<>();
for (int i = 0; i < 40; i++) {
String key = String.valueOf(i);
new Thread(()->{
// 向集合中添加数据
// set.add(UUID.randomUUID().toString().substring(0,8));
hashMap.put(key,UUID.randomUUID().toString().substring(0,8));
// 从集合中取数据
System.out.println(hashMap);
},String.valueOf(i)).start();
}
打印结果:
ConcurrentHashMap
类是HashMap的实现类
将其代码修改为
Map<String,String> map = new ConcurrentHashMap<>();
通过这行代码可以编程线程安全问题