JavaSE自学笔记017_Real(多线程中的锁)
一、synchronized简介
在多线程并发编程中synchronized一直是元老级的角色,synchronized有三种方式来加锁,分别是:
1、修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁;
2、静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁;
3、修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
分类 | 具体分类 | 被锁对象 | 伪代码 |
---|---|---|---|
方法 | 实例方法 | 调用该方法的实例对象 | public synchronized void method() |
方法 | 静态方法 | 类对象class对象 | public static synchronized void method() |
代码块 | this | 调用该方法的实例对象 | synchronized(this){ } |
代码块 | 类对象 | 类对象 | synchronizedDemo.class){ } |
代码块 | 任意的实例对象 | 创建的实例对象 | Object lock = new Object(); synchronized(lock){ } |
二、死锁
死锁:多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源的释放,由于线程被无限期阻塞,因此程序不可能终止。
Java产生死锁的四个必要条件:
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用。
2、不可抢占,资源请求者不能抢占资源拥有者手中的资源,必须由资源拥有者主动释放资源;
3、请求和保持,即当资源请求者在请求其他资源的时候同时保持者对原有资源的占有;
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源,这样形成了一个等待环路。
上述四个条件都成立的时候,便会形成死锁,当然,死锁的情况下如果打破上述任意一个条件,死锁便会立即消失你。
三、wait和notify
package com.ThreadStudy;
public class waitTest {
public static final Object MONITOR = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (MONITOR){
System.out.println("线程1开始了");
try {
MONITOR.wait(); //中断线程1========
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1结束了。。。");
}
}).start();
new Thread(() -> {
synchronized (MONITOR) {
System.out.println("线程2开始了");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
MONITOR.notify(); //唤醒线程1===========
System.out.println("线程2结束了。。。。");
}
}).start();
}
}
方法总结:
Thread.sleep():释放CPU资源,但是不释放锁。
Thread.yield():释放了CPU的执行权,但是依然保留了CPU的执行资格(该方法不常用)
四、LockSupport静态类
package com.ThreadStudy;
import java.util.concurrent.locks.LockSupport;
public class LockSupportTest {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println(1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(2);
LockSupport.park();
System.out.println(3);
});
thread.start();
thread.sleep(5000);
System.out.println("五秒过去了。。。。。");
LockSupport.unpark(thread);
}
}
五、Lock锁
Lock接口有几个重要的方法:
// 获取锁
void lock();
// 仅在调用时锁为空闲状态才获取该锁,可以响应中断
boolean tryLock();
// 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁
boolean tryLock(Long time, TimeUnit unit)
//释放锁
void unlock();
获取锁,两种写法
Lock lock = new Lock();
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
六、并发编程的三大特性
1、原子性
原子性定义:原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。将整个操作视为一个整体是原子性的核心特征,原子性不仅仅是多行代码,也可能是多条指令。
一行代码一定具有原子性吗?编译成字节码->机器码,不一定具有原子性。
存在竞争条件,线程不安全,需要转变原子操作才能安全,方式:上锁,循环CAS; 上例只是针对一个变量的原子操作的改进
2、可见性
3、有序性
七、CAS
问题描述:
当多线程执行程序的时候,每一个线程会开一个栈,但是数据存放在主存中,主存只有一个,线程从主存中取数据,比如多个线程执行一个COUNT++的操作,每一个操作要执行【取数据】【压栈】【弹栈】【加一】【存入主存】等操作,但是,比如现在COUNT在主存中的数据为3,如果一个线程取数据放入在栈中,恰好另一个线程也去取数据,两个线程取出的数据都是3,都进行了加一操作,都变成了4,则会出现错误,本来应该所有线程执行完COUNT会变成500,但是实际运行之后达不到500,就会出现问题,为了解决这一问题提出了Compare and Swap的解决方案。
目前新的JDK标准已经变成了Compare and Set
CAS(Compare of Swap的缩写)
Java中的CAS是通过sun.misc.Unsafe类提供,来保证CAS的
CAS还有几个缺点:
(1)ABA问题:当地一个线程执行CAS操作,尚未修改新值之前,内存忠厚的值已经被其线程连续修改了两次,使得变量经历了A->B->A的过程,绝大部分情况我们对于ABA问题不敏感。解决方案:添加版本号作为标识,每次修改变量的时候,对应版本号增加,做CAS操作的时候需要校验版本号,JDK1.5之后,新增AtomicStampedReference类来处理这种情况。
(2)循环时间长开销大,如果有很多个线程并发,CAS自旋可能会长时间不成功,会增大CPU的执行开销
(3)只能对一个变量进行原子操作,JDK1.5之后,新增AtomicReference类来处理这种情况,可以将多个变量放到一个对象中去。
package com.ThreadStudy;
public class AtomicTest {
public static volatile int COUNT;
//CAS
public static synchronized boolean CompareAndSwap(int except, int update){
if(except == COUNT){
COUNT = update;
return true;
}
return false;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 500; i++) {
new Thread(() -> {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 自旋
boolean flag = false;
while (!flag){
flag = CompareAndSwap(COUNT, COUNT + 1);
}
}).start();
}
Thread.sleep(3000);
System.out.println(COUNT);
}
}
八、aqs
抽象队列同步器,用来解决线程同步执行的问题 (AbstractQueueSynchronizer)
解决思路如下:
九、原子类
案例:
package com.AtomicTest;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerTest {
private static AtomicInteger atomicinteger = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 200; i++) {
new Thread(() -> {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicinteger.getAndIncrement();
}).start();
}
Thread.sleep(3000);
System.out.println(atomicinteger.get());
}
}
十、线程池
1、jdk自带的四种线程池
Java通过Executors提供四种线程池,分别是:
(1)newCachedThreadPool创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若没有回收,则创建新线程。
(2)newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超过的线程会在队列中等待。
(3)newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行。
(4)newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有的任务按照指定顺序执行。
import java.util.concurrent.Executors;
public class ThreadPoolTest {
public static void main(String[] args) {
//ExecutorService executorService = Executors.newFixedThreadPool(5);
//ExecutorService executorService = Executors.newSingleThreadExecutor();
ExecutorService executorService = Executors.newCachedThreadPool();
Runnable task = () -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("---------------------");
};
for (int i = 0; i < 100; i++) {
executorService.submit(task);
}
}
}
2、线程池类初始化参数的意义
corePoolSize:指定了线程池里的线程数量,核心线程大小。
maximumPoolSize:定了线程池的最大线程数量。
keepAliveTime:当线程池数量大于corePoolSize的时候,多出来的空闲线程,多长时间会被销毁。
unit:时间单位,TimeUnit
workQueue:任务队列,用于村方提交但是尚未被执行的任务。
threadQueue:线程工厂,用于创建线程,线程工厂就是给我们new线程的。
handler:所谓拒绝策略,是指将任务添加到线程池中的时候,线程池拒绝该任务所采取的相应策略。
常见的工作队列有如下选择,这些都是阻塞队列,阻塞队列的意思是,当队列中没有值的时候,取值操作会阻塞,一直等待队列中产生值
ArrayBlockingQueue:基于数组结构的优解阻塞队列 FIFO 有界队列
LinkedBlockQueue:基于链表结构的游街阻塞队列 FIFO 无界队列
线程池提供了四种拒绝策略:
(1)AbortPolicy:直接抛出异常,默认策略
(2)CallerRunsPolicy:用调用者所在的线程执行任务
(3)DiscardOldestPolicy:丢弃阻塞队列中靠最前面的任务,并执行当前任务
(4)DiscardPolicy:直接丢弃任务
3、自定义线程
package com.PoolTest;
import java.util.concurrent.*;
public class CustomThreadPoolTest {
public static void main(String[] args) {
//自定义线程池
ExecutorService executorService = new ThreadPoolExecutor(5, 10,
60L, TimeUnit.MINUTES,
new ArrayBlockingQueue<>(50),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r);
}
},
(r, executor) -> {
System.out.println("任务太多了,这个任务没法处理了");
});
//循环创建线程,数量不得超过60 因为最大线程数为10,队列最大等待线程数位50,所以最多容纳60哥线程,超过则会抛异常。
for (int i = 0; i < 300; i++) {
executorService.submit(() -> {
System.out.println("------------------");
});
}
//关闭线程池
executorService.shutdown();
}
}
4、线程工厂
Goole.guava工具类提供的THreadFactoryBuilder
Apache commons-lang3提供的BasicThreadFactory
5、线程同步
CountDownLanch的使用
设置倒计数,有线程执行完就会将计数器减一,当达到0的时候,就会执行CountDownLanch中的线程。
CycleBarier的使用
设置计数器,有线程执行完就会加一,当达到某一数值的时候,就会执行CycleBarier中的线程。两者区别,后者可以利用reset()函数进行重新设置,就可以循环使用,但是前者只能用一次就会自动销毁。
6、Semaphore(信号量)
用于限制同时工作的线程数,比如最多只有十个线程运行,运行完一个,其他线程才能进入工作。