总结:
每个对象有一个监视器锁monitor,线程进入同步方法时尝试获取monitor的所有权,其他线程进入阻塞状态。该线程释放monitor的所有权后其他线程重新尝试获取monitor的所有权。
只能有一个线程对同步监视器加锁
1、多线程的问题案例
两个取钱线程,取出来剩余金额为负数
2、synchronize优化多线程
我们选用共享资源(account对象)作为同步监视器, 只有一个线程可以获得同步监视器的锁定。
方式一:在线程里对对象加锁
方式二:在对象中对方法加锁
3、同步锁 Lock实现多线程同步
4、线程死锁
案例:线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
结果:
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1
结果说明:
线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。
死锁的四个必要条件。
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
3. 如何避免线程死锁?
破坏循环等待:将系统中的所有资源统一编号,进程在可在任何时候提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。
我们对上面的代码进行修改,将线程 2 的代码修改成下面这样就不会产生死锁了。
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 2").start();
结果:
Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2
Process finished with exit code 0
线程通信
(1)、synchronize和lock有什么区别?底层都是如何实现的?synchronize是不是可重入锁?
区别:
a、synchronized原始采用的是CPU悲观锁机制,通过monitor对象来完成。而Lock用的是乐观锁方式,采用AQS+CAS的方式进行保证线程安全。所以lock在大量的线程同步下的效率会高。
b、锁的释放方面,synchronized是由jvm来进行控制的,Lock是需要自己释放锁的。
c、是否可以中断:synchronize不可中断,ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。
d、是否公平锁:synchronized为非公平锁 ReentrantLock则即可以选公平锁也可以选非公平锁,通过构造方法new ReentrantLock时传入boolean值进行选择,为空默认false非公平锁,true为公平锁,公平锁性能非常低。
【“公平锁和非公平锁的区别在于获取锁的顺序。公平锁是按照申请锁的顺序,让线程直接进入队列排队,只有队列中的第一个线程才能获取锁。优点是所有线程都能获取到资源,不会阻塞在队列中。缺点是吞吐量会下降,因为其他线程可能会阻塞,而且CPU唤醒阻塞线程的开销也会增加。非公平锁是多个线程尝试获取锁时,会直接进入等待队列,如果能获取到,就直接获取到锁。优点是可以减少CPU唤醒线程的开销,整体的吞吐效率会稍高】
共同点:
可重入:synchronized和lock都是可重入锁。
AQS:
AQS全称:AbstractQueuedSynchronizer,抽象队列式同步器。
AQS是一个抽象类,它定义了一套多线程访问共享资源的同步器框架。通俗解释,AQS就像是一个队列管理员,当多线程操作时,对这些线程进行排队管理。
AQS主要通过维护了两个变量来实现同步机制的。AQS使用一个volatile修饰的私有变量state来表示同步状态,当state=0表示释放了锁,当state>0表示获得锁。
AQS通过内置的FIFO同步队列,来实现线程的排队工作。如果线程获取当前同步状态失败,AQS会将当前线程的信息封装成一个Node节点,加入同步队列中,并且阻塞该线程,当同步状态释放,则会将队列中的线程唤醒,重新尝试获取同步状态。
(2)、synchronize锁对象和锁方法有什么区别?
Synchronized方法锁、对象锁、类锁区别 - 不懂就查 - 博客园
synchronized的底层实现原理 - 望川拓 - 博客园
a、synchronize作用在代码块上,通过指令monitorenter和monitorexit来完成。每个对象有一个监视器锁:monitor。线程执行monitorenter指令时尝试获取monitor的所有权,其他线程进入阻塞状态。执行monitorexit的线程释放monitor的所有权,其他线程重新尝试获取monitor的所有权。
b、作用在方法上时通过ACC_SYNCHRONIZED标示符来指定线程锁定了monitor对象。
JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
(3)、悲观锁乐观锁的区别和应用。乐观锁的CAS机制。AtomicInter的底层实现。
- synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
- CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
乐观锁CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值(这个过程中可能被其他线程修改了)相同时,才会将内存地址V对应的值修改为B。
(4)、线程间通信的方式
信号量机制
synchronize中是wait,notifyAll
lock中是await和signalAll
(5)、threadLocal
每个线程一个新的变量
ThreadLocal的数据结构:ThreadLocal是一个类里面用Thead的ThreadLocalMap属性进行存储数据。其中key是ThreadLocal对象的弱引用,value是存储的数据。
ThreadLocal容易出现内存泄漏的原因:ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。
使用案例 1、每个线程需要一个独享对象,例如SimpleFormatDate,SimpleFormatDate是线程不安全的,里面有calcaulate.set操作,当多线程访问时会出现冲突。所以可以将SimpleFormatDate封装为ThreadLocal类型。
(6)、volatile
简单总结下,volatile是一种轻量级的同步机制,它主要有两个特性:一是保证共享变量对所有线程的可见性;同时需要注意的是,volatile对于单个的共享变量的读/写具有原子性,但是像num++这种复合操作,volatile无法保证其原子性,当然文中也提出了解决方案,就是使用并发包中的原子操作类,通过循环CAS地方式来保证num++操作的原子性。二是禁止指令重排:在多线程操作情况下,指令重排会导致计算结果不一致
/**
* volatile 关键字,使一个变量在多个线程间可见
* A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
* 使用volatile关键字,会让所有线程都会读到变量的修改值
加了volatile之后,2个线程之间的数据就可以及时可见。保证2个线程之间,变量的可见性。 volatile并不能解决加锁的问题。
(7)、线程池的原理,核心参数的含义,有哪些拒绝策略
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
corePoolSize:线程池的大小。线程池创建之后不会立即去创建线程,而是等待线程的到来。当当前执行的线程数大于该值时,线程会加入到缓冲队列;
maximumPoolSize:线程池中创建的最大线程数;
keepAliveTime:空闲的线程多久时间后被销毁。默认情况下,该值在线程数大于corePoolSize时,对超出corePoolSize值得这些线程起作用。
unit:TimeUnit枚举类型的值,代表keepAliveTime时间单位,可以取下列值:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
workQueue:阻塞队列,用来存储等待执行的任务,决定了线程池的排队策略,有以下取值:
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者。使用SynchronousQueue队列,提交的任务不会被保存,总是会马上提交执行。如果用于执行任务的线程数量小于maximumPoolSize,则尝试创建新的进程,如果达到maximumPoolSize设置的最大值,则根据你设置的handler执行拒绝策略。因此这种方式你提交的任务不会被缓存起来,而是会被马上执行。其他阻塞队列会被延迟执行。
threadFactory:线程工厂,是用来创建线程的。默认new Executors.DefaultThreadFactory();
handler:线程拒绝策略。当创建的线程超出maximumPoolSize,且缓冲队列已满时,新任务会拒绝,有以下取值:
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
线程池按以下行为执行任务
1. 当线程数小于核心线程数时,创建核心core线程。
2. 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
3. 当线程数大于等于核心线程数,且任务队列已满
- 若线程数小于最大线程数,创建线程
- 若线程数等于最大线程数,抛出异常,拒绝任务
<bean id="suggestThreadPool" class="java.util.concurrent.ThreadPoolExecutor" >
<constructor-arg index="0" type="int" value="4"/>
<constructor-arg index="1" type="int" value="30"/>
<constructor-arg index="2" type="long" value="60"/>
<constructor-arg index="3" type="java.util.concurrent.TimeUnit" value="SECONDS"/>
<constructor-arg index="4" type="java.util.concurrent.BlockingQueue">
<bean class="java.util.concurrent.SynchronousQueue">
</bean>
</constructor-arg>
<constructor-arg index="5" type="java.util.concurrent.RejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy"/>
</constructor-arg>
</bean>
Excutor类:
public static ExecutorService getExecutor() {
if(executor == null){
synchronized (Executor.class){
if(executor == null){
executor = Executors.newCachedThreadPool();
}
}
}
return executor;
}
使用过程:
Future<List<BroadListModel>> mainFuture = Executor.getExecutor().submit(new Callable<List<BroadListModel>>() {
@Override
public List<BroadListModel> call() throws Exception {
return broadListDao.getBroadListTrend(params);
}
});
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class MutiThread {
public static ExecutorService getExecutor() {
ExecutorService executor = null;
if(executor == null){
synchronized (Executor.class){
if(executor == null){
executor = Executors.newCachedThreadPool();
}
}
}
return executor;
}
public static void main(String[] args) {
List<Future> futures = new ArrayList<>(3);
for (int i=0;i<3;i++) {
int finalI = i;
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
if(finalI==2){
Thread.sleep(6000);
}
return finalI;
}
};
futures.add(getExecutor().submit(callable));
}
for (Future future : futures) {
try {
Object result = future.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("finish");
}
}
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
下面我们来分析newCachedThreadPool:
这种类型的线程池特点是:
工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
8、怎么保证线程按顺序执行
强行进入使用join方法的线程,其他线程等待该线程完全执行完后才会进来。
1)现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?_xuehuagongzi000的博客-CSDN博客
thread1.start();
thread1.join();
thread2.start();
thread2.join();
thread3.start();
thread3.join();
9、常见面试题
手写多线程交替打印ABC
package com.demo.test;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class syncPrinter implements Runnable{
// 打印次数
private static final int PRINT_COUNT = 10;
private final ReentrantLock reentrantLock;
private final Condition thisCondtion;
private final Condition nextCondtion;
private final char printChar;
public syncPrinter(ReentrantLock reentrantLock, Condition thisCondtion, Condition nextCondition, char printChar) {
this.reentrantLock = reentrantLock;
this.nextCondtion = nextCondition;
this.thisCondtion = thisCondtion;
this.printChar = printChar;
}
@Override
public void run() {
// 获取打印锁 进入临界区
reentrantLock.lock();
try {
// 连续打印PRINT_COUNT次
for (int i = 0; i < PRINT_COUNT; i++) {
//打印字符
System.out.print(printChar);
// 使用nextCondition唤醒下一个线程
// 因为只有一个线程在等待,所以signal或者signalAll都可以
nextCondtion.signal();
// 不是最后一次则通过thisCondtion等待被唤醒
// 必须要加判断,不然虽然能够打印10次,但10次后就会直接死锁
if (i < PRINT_COUNT - 1) {
try {
// 本线程让出锁并等待唤醒
thisCondtion.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} finally {
reentrantLock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
Condition conditionC = lock.newCondition();
Thread printA = new Thread(new syncPrinter(lock, conditionA, conditionB,'A'));
Thread printB = new Thread(new syncPrinter(lock, conditionB, conditionC,'B'));
Thread printC = new Thread(new syncPrinter(lock, conditionC, conditionA,'C'));
printA.start();
Thread.sleep(100);
printB.start();
Thread.sleep(100);
printC.start();
}
}
题目二:
有100个任务需要分成10批执行,每批执行有顺序(即第一批执行完执行第二批)。
- 说明:10批任务有序执行,每批任务的10个任务要做到并发执行
//===========================================================================
//方案A——考察CountDownLatch熟练使用
private static void planA() throws Exception {
for (int i = 1; i <= OUT; i++) {
CountDownLatch cd = new CountDownLatch(10);
for (int j = 0; j < INNER; j++) {
int finalJ = j;
new Thread(() -> {
try {
// do something
// 模拟任务耗时不同
Thread.sleep(new Random().nextInt(10));
System.out.println(finalJ);
} catch (Exception e) {
e.printStackTrace();
} finally {
cd.countDown();
}
})
.start();
}
cd.await();
System.out.println("第" + i + "批结束");
}
}