目录
5.2 ArrayList线程不安全和解决方案——vector
5.3 ArrayList线程不安全和解决方案——Collections
5.4 ArrayList线程不安全和解决方案——CopyOnWriteArrayList(推荐)
1.JUC概述与进程线程
1.1 JUC概述
JUC是java.util.concurrent工具包的简称,处理线程的工具包。
1.2 进程与线程
进程:指在系统上正在运行的一个应用程序,程序一旦运行就是进程;进程——资源分配的最小单元。
线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流;线程——程序执行的最小单位。
eg:当我们打开一个应用软件时就会创建一个系统进程,当我们使用这个应用软件内部的某个功能时就相当于开启了一个线程。
1.3 wait与sleep
(1)sleep是JAVA中Thread的静态方法,也就说明sleep只能被当前类来调用;wait是object方法,任何对象实例都能调用。
(2)sleep不会释放锁也不需要占有锁;wait会释放锁,但调用它的前提是当前线程占有锁,也就是需要被synchronize所修饰。
(3)都会被interrupted方法中断。
1.4 并发与并行
并发:同一时刻多个线程访问同一资源;多个线程对同一个点,比如商品秒杀。
并行:多项工作一起执行,然后在汇总。比如泡方便面,需要烧热水,然后烧热水的同时需要撕开调料...,最终汇总前面的一系列操作来完成这个工作。
1.5 用户进程和守护进程
用户线程:自定义线程 ;主线程结束了,用户线程还在运行,JVM存活。
public class Main {
public static void main(String[] args) {
Thread myThread = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "::是否守护线程:" + Thread.currentThread().isDaemon());
while (true) ;
}, "myThread");
myThread.start();
System.out.println("main::over");
}
}
当主线程结束后,程序依旧处于运行状态;
守护线程:比如垃圾回收机制;没有用户线程了,剩下的都是守护线程,JVM结束。
public class Main {
public static void main(String[] args) {
Thread myThread = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "::是否守护线程:" + Thread.currentThread().isDaemon());
while (true) ;
}, "myThread");
//将用户线程设置为守护线程
myThread.setDaemon(true);
myThread.start();
System.out.println("main::over");
}
}
当主线程结束后,由于用户线程被设置为守护线程,JVM会结束。
2.Lock接口
2.1 Synchronized关键字
synchronized是java中的关键字,是一种同步锁。它修饰的对象有以下几种。
(1)修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是{}括起来的代码,作用对象是调用这个代码块的对象。
(2)修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。
(3)修饰一个静态方法,其作用范围是整个静态方法,作用的对象是这个类的所有对象。
(4)修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用对象是这个类的所有对象。
synchronized关键字是不能被继承的,也就是说在父类中有一个被synchronized修饰的方法,在其子类中这个方法被重写时,子类中的这个方法默认是不同步的,但是可以在子类的方法上也加上synchronized关键字,或者在子类中调用父类的同步方法来实现子类方法的同步。
2.2 多线程编程步骤(上)
(1)创建资源类,在资源类创建属性和操作方法。
(2)创建多个线程,调用资源类的操作方法。
2.2 synchronized售票案例
3个售货员卖出30张票
(1)创建资源类,在资源类中定义属性和方法
新建项目,创建一个资源类:
public class Ticket {
private int ticketNum=30;
public synchronized void saleTicket(){
//用来存放当前用户卖票数量;
int tmp=0;
//判断是否有票
if(ticketNum>0)
{
System.out.println(Thread.currentThread().getName()+"卖出:"+(++tmp)+"剩余:"+(--ticketNum));
}
}
}
(2)创建多个线程,调用资源类的操作方法。
在当前项目下创建一个执行类,创建多个线程来调用同一个资源类的操作方法。
//创建多个线程,调用资源类的方法
public class Operation {
public static void main(String[] args) {
Ticket ticket=new Ticket();
Thread sale1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<40;i++)
{
ticket.saleTicket();
}
}
}, "sale1");
Thread sale2 = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<40;i++)
{
ticket.saleTicket();
}
}
}, "sale2");
Thread sale3 = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<40;i++)
{
ticket.saleTicket();
}
}
}, "sale3");
sale1.start();
sale2.start();
sale3.start();
}
}
运行程序。
ps:synchronized关键字实现的锁是自动的,我们若需要手动上锁则需要使用到Lock接口。
2.3 JUC_Lock接口概述和实现案例
2.3.1 什么是Lock
所在包:java.util.concurrent.locks
为锁和等待条件提供了一个框架接口和类,它不同于内置同步和监视器。
Lock锁实现了提供比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock提供了比synchronized更多的功能。
2.3.2 Lock与Synchronized的区别
(1)Lock不是java语言内置的,synchronized是java语言的关键字,因此是内置特性,Lock是一个接口,通过这个接口的实现类可以实现同步访问。
(2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,而Lock则必须要手动释放锁。如果没有手动释放锁,则可能出现死锁现象。
(3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断。
(4)通过Lock可以知道有没有获取锁,而synchronized却无法办到。
2.3.3 使用Lock实现售票案例
(1)创建资源类,在资源类中定义属性和方法。
public class Ticket {
private int ticketNum=30;
//保证是同一把锁
private final ReentrantLock lock=new ReentrantLock();
public void saleTicket(){
try{
lock.lock();
//用来存放当前用户卖票数量;
int tmp=0;
//判断是否有票
if(ticketNum>0)
{
System.out.println(Thread.currentThread().getName()+"卖出:"+(++tmp)+"剩余:"+(--ticketNum));
}
}
finally {
lock.unlock();
}
}
}
(2)创建多个线程,调用资源类的操作方法。
public class Operation {
public static void main(String[] args) {
Ticket ticket=new Ticket();
Thread sale1 = new Thread(() -> {
for(int i=0;i<40;i++){
ticket.saleTicket();
}
}, "sale1");
Thread sale2= new Thread(() -> {
for(int i=0;i<40;i++){
ticket.saleTicket();
}
}, "sale2");
Thread sale3= new Thread(() -> {
for(int i=0;i<40;i++){
ticket.saleTicket();
}
}, "sale3");
sale1.start();
sale2.start();
sale3.start();
}
}
3.线程间通信
3.1多线程编程步骤(中)
(1)创建资源类,在资源类创建属性和操作方法
(2)判断;干活;通知
(3)创建多个线程,调用资源类的操作方法
3.2 案列(synchronized)
创建两个线程,一个变量,初值为0,一个线程对其加一,一个线程对其减一。
(1)创建资源类
public class Resource {
private int num=0;
public synchronized void incr() throws InterruptedException {
//判断
if(num!=0)
{
this.wait();
}
//干活
num++;
System.out.println(Thread.currentThread().getName()+":"+num);
//通知 唤起等待池的所有线程
this.notifyAll();
}
public synchronized void decr() throws InterruptedException {
if(num!=1)
{
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName()+":"+num);
this.notifyAll();
}
}
(2)创建执行程序
public class Operation {
public static void main(String[] args) {
Resource resource=new Resource();
Thread incr = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
resource.incr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "incr");
Thread decr=new Thread(()->{
try {
for (int i = 0; i < 20; i++) {
resource.decr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"decr");
incr.start();
decr.start();
}
}
(3)运行程序。
incr:1
decr:0
incr:1
decr:0
incr:1
decr:0
incr:1
decr:0
3.3 虚假唤醒问题
将3.2中的执行方法复制两份,变成4个方法。运行查看结果。
public static void main(String[] args) {
Resource resource=new Resource();
Thread incr1 = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
resource.incr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "incr1");
Thread decr1=new Thread(()->{
try {
for (int i = 0; i < 20; i++) {
resource.decr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"decr1");
Thread incr2 = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
resource.incr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "incr2");
Thread decr2=new Thread(()->{
try {
for (int i = 0; i < 20; i++) {
resource.decr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"decr2");
incr1.start();
decr1.start();
incr2.start();
decr2.start();
}
}
incr1:1
decr1:0
incr2:1
decr2:0
decr1:-1
incr1:0
incr1:1
decr1:0
decr2:-1
incr2:0
incr2:1
decr2:0
decr1:-1
分析结果产生原因:wait()方法是在哪里睡在哪里醒。
假设初值为0;
incr1 | decr1 | incr2 | decr2 | 抢占线程 | 初值改变 |
---|---|---|---|---|---|
1 | incr1 | 1 | |||
wait | incr2 | 1 | |||
wait | wait | incr1 | 1 | ||
0 | decr1 | 0 | |||
1 | incr1 | 1 | |||
2 | incr2 | 2 |
由于在incr1和incr2都等待后,decr1唤醒了两个等待线程,则incr1和incr2会从wait()方法处继续往下运行,此时先由incr1抢占线程,然后incr2抢占线程,那么值就会变成2,出现虚假唤醒问题。
解决方案:将if改为while循环。则每次唤醒线程后都需要在判断一次。
3.4 案例(Lock)
(1)创建资源类
在lock中的等待和唤醒方法需要使用Lock实例对象创建出Condition对象,调用Condition的方法await()和signalAll()来实现。
public class LockResource {
private int num=0;
ReentrantLock lock=new ReentrantLock();
Condition condition=lock.newCondition();
public void incr() {
try{
lock.lock();
while (num!=0)
{
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName()+":"+num);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decr() {
try{
lock.lock();
while (num!=1)
{
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName()+":"+num);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
(2)创建执行类
public class LockOperation {
public static void main(String[] args) {
LockResource lockResource=new LockResource();
new Thread(()->{
for(int i=0;i<40;i++)
{
lockResource.incr();
}
},"incr").start();
new Thread(()->{
for(int i=0;i<40;i++)
{
lockResource.decr();
}
},"decr").start();
}
}
(3)执行程序。
incr:1
decr:0
incr:1
decr:0
incr:1
decr:0
incr:1
decr:0
4.进程间定制化通信
4.1案例
启动三个线程,按照如下要求:
AA打印5次,BB打印10次,CC打印15次
进行10轮;
思路:在资源类中创建三个方法,分别打印AA,BB,CC使用标志位来记录执行哪个线程,例如:AA==>1;BB==>2;CC==>3;
当AA执行完成之后,修改标志位为2,然后通知BB执行,BB执行完成后,修改标志位为3,通知CC执行,依次类推;
代码如下:
(1)创建资源类
public class Resource {
private int flag=1;
Lock lock=new ReentrantLock();
Condition condition1=lock.newCondition();
Condition condition2=lock.newCondition();
Condition condition3=lock.newCondition();
public void printAA(int lun)
{
try{
lock.lock();
while (flag!=1)
{
condition1.await();
}
for (int i = 0; i <5 ; i++) {
System.out.println(Thread.currentThread().getName()+":AA"+",轮数:"+(lun*1+1.0));
}
flag=2;
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printBB(int lun)
{
try{
lock.lock();
while (flag!=2)
{
condition2.await();
}
for (int i = 0; i <10 ; i++) {
System.out.println(Thread.currentThread().getName()+":BB"+",轮数:"+(lun*1+1.0));
}
flag=3;
condition3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printCC(int lun)
{
try{
lock.lock();
while (flag!=3)
{
condition3.await();
}
for (int i = 0; i <15 ; i++) {
System.out.println(Thread.currentThread().getName()+":CC"+",轮数:"+(lun*1+1.0));
}
flag=1;
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
(2)创建执行
public class Operation {
public static void main(String[] args) {
Resource resource=new Resource();
Thread printAA = new Thread(() -> {
for (int i = 0; i < 10; i++) {
resource.printAA(i);
}
}, "printAA");
Thread printBB = new Thread(() -> {
for (int i = 0; i < 10; i++) {
resource.printBB(i);
}
}, "printBB");
Thread printCC = new Thread(() -> {
for (int i = 0; i < 10; i++) {
resource.printCC(i);
}
}, "printCC");
printAA.start();
printBB.start();
printCC.start();
}
}
5.集合线程安全
5.1 集合线程不安全演示
以List集合为例;list的add()方法是没有被synchronized修饰,所以在多个线程调用时会出现异常。
代码演示:
public class Test {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
异常:java.util.ConcurrentModificationException
5.2 ArrayList线程不安全和解决方案——vector
public class ResolveVector {
public static void main(String[] args) {
List<String> list=new Vector<>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
使用vector实现类,实现类的add()方法中加入了synchronized关键字;
5.3 ArrayList线程不安全和解决方案——Collections
在Collections中有一个synchronizedList(List<T> list)静态方法,能够返回指定列表支持同步的列表;
public class ResolveCollections {
public static void main(String[] args) {
List<String> list= Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 20; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
5.4 ArrayList线程不安全和解决方案——CopyOnWriteArrayList(推荐)
public class ResolveCopyOnWriteArrayList {
public static void main(String[] args) {
List<String> list= new CopyOnWriteArrayList<>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
CopyOnWrite:写时复制技术,当需要写入时,先复制一个list,在复制的list中进行写入,写入的过程中如果有读操作,则读取原来的内容。然后合并新旧list。既兼顾了并发读也实现了独立写。
源码分析:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取到List集合数组
Object[] elements = getArray();、
//获取数组长度
int len = elements.length;
//复制一份list集合数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//将新的元素插入到数组最后面
newElements[len] = e;
//将新list数组覆盖旧的
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
5.5 HashSet和HashMap线程不安全
(1)HashSet演示 解决ArrSet
public class TestHashSet {
public static void main(String[] args) {
Set<String> set=new HashSet<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
异常:ConcurrentModificationException
(2)HashMap演示
public class TestHashSet {
public static void main(String[] args) {
Map<String,String> map=new HashMap<>();
for (int i = 0; i < 10; 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();
}
}
}
异常:ConcurrentModificationException
5.6 HashSet和HashMap线程不安全解决方案
(1)HashSet解决方案——CopyOnWriteArraySet
public class TestHashSet {
public static void main(String[] args) {
Set<String> set=new CopyOnWriteArraySet<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,8));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
(2)HashMap解决方案——ConcurrentHashMap
public static void main(String[] args) {
Map<String,String> map=new ConcurrentHashMap<>();
for (int i = 0; i < 10; 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();
}
}
5.7 小结
List与Set推荐使用写时复制技术。Map使用ConcurrentHashMap。
6.synchronized锁的八种情况
(1)编写资源类
public class Phone {
public static synchronized void sendSMS() throws Exception{
TimeUnit.SECONDS.sleep(4);
System.out.println("------sendSMS");
}
public synchronized void sendEmail() throws Exception{
System.out.println("------sendEmail");
}
public void getHello(){
System.out.println("------getHello");
}
}
(2)编写执行类
public class Operation {
public static void main(String[] args) throws Exception {
Phone phone=new Phone();
Phone phone1=new Phone();
new Thread(()->{
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
},"AA").start();
Thread.sleep(100);
new Thread(()->{
try {
//phone.getHello();
phone1.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
},"BB").start();
}
}
(3)八种情况及说明
/**
* 1.标准访问,先打印短信还是邮件
* 答: ------sendSMS
* ------sendEmail
* 原因:锁的对象是整个对象,由于调用两个线程之间停留了100ms,所以会先执行sendSMS,然后在执行sendEmail
* 2.停4秒在短信方法内,先打印短信还是邮件
* 答:------sendSMS
* ------sendEmail
* 原因:锁的对象是整个对象,由于调用两个线程之间停留了100ms,所以会先执行sendSMS,然后等待4s之后在执行sendEmail
* 3.新增普通的hello方法,是先打印短信还是hello
* 答:------getHello
* ------sendSMS
* 原因:普通方法与锁无关
* 4.现有两部手机,是先打印短信还是邮件
* 答:------sendEmail
* ------sendSMS
* 原因:不是同一把锁;
* 5.两个静态同步方法,1部手机,先打印短信还是邮件
* 答:------sendSMS
* ------sendEmail
* 原因:锁的范围是整个类
* 6.两个静态同步方法,2部手机,先打印短信还是邮件
* 答:------sendSMS
* ------sendEmail
* 原因:锁的范围是整个类,一部手机和两部手机都是同一把锁
* 7.1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
* 答:------sendEmail
* ------sendSMS
* 原因:锁的对象不一样,好比静态的锁像一栋大楼,而普通的锁是大楼里面的房间;
* 8.1个静态同步方法,一个普通同步方法,2部手机,先打印短信还是邮件
*答:------sendEmail
* ------sendSMS
* 原因:与7一样。
*/
(4)小结
synchronized实现同步的基础:java中的每一个对象都可以作为锁。
具体表现为以下三种形式。
-
对于普通同步方法,锁是当前实例对象
-
对于静态同步方法,锁是当前类的Class对象
-
对于同步方法块,锁是synchronized括号里配置的对象
7.公平锁和非公平锁
//使用无参构造或者传参为false则为非公平锁,传参为true则为公平锁
ReentrantLock lock=new ReentrantLock();
非公平锁:线程可能会饿死(有的线程不干事),但是效率高;
公平锁:阳光普照,但是效率相对较低;
8.可重入锁(递归锁)
synchronized与Lock都是可重入锁,synchronized(隐式的),Lock(显示的);
eg:进家门时打开大门的锁,其余门可以自由进入。通俗来讲就是说使用的是同一把锁。
8.1 synchronized案例
public class SynchronizedTest {
public static void main(String[] args) {
Object o=new Object();
synchronized (o){
new Thread(()->{
System.out.println(Thread.currentThread().getName()+":外层");
new Thread(()->{
System.out.println(Thread.currentThread().getName()+":中层");
new Thread(()->{
System.out.println(Thread.currentThread().getName()+":内层");
},"t3").start();
},"t2").start();
},"t1").start();
}
}
}
输出结果:
t1:外层
t2:中层
t3:内层
8.2 Lock案例
public class LockTest {
public static void main(String[] args) {
Lock lock=new ReentrantLock();
new Thread(()->{
try{
lock.lock();
System.out.println(Thread.currentThread().getName()+":外层");
new Thread(()->{
try{
lock.lock();
System.out.println(Thread.currentThread().getName()+":内层");
}finally {
lock.unlock();
}
},"t2").start();
}finally {
lock.unlock();
}
},"t1").start();
}
}
输出结果:
t1:外层
t2:内层
ps:使用Lock时有上锁就必须有解锁,要是Lock里面的代码段被上锁却没解锁,虽然对整个线程没影响,却会影响别的线程。