文章目录
背景
多线程想必大家都不陌生,主要用来在高并发操作时提升性能的同时也带来了并发的问题,那么本篇博客在于梳理和总结多线程的一些知识,如果不对请多多指教及时改正。
理解并发与并行
- 并行:只两个或者多个事情在同一时刻发生
- 并发:两个或者多个事件在同一时间间隔发生
synchronized与volatile
- 好了,废话不多说直接上硬菜,想必接触过多线程大家都不陌生synchronized和volatile这两个字段.那么这两个字段有什么区别呢?
区别
synchronized可以保证事件的原子性和可见性。
volatile只能保证事件的可见性。
这么说想必大家肯定还是不明白,那么就通过实例加注解来详细描述~
- 使用synchronized关键字
public class T_09_Synchronized implements Runnable{
public static void main(String[] args) {
T_09_Synchronized t_09_synchronized = new T_09_Synchronized();
for(int i=0;i<5;i++){
//起五个线程
new Thread(t_09_synchronized).start();
}
}
private int count = 10;
//使用synchronized关键字,保证了线程原子性,多个线程并发操作不影响结果
@Override
public synchronized void run() {
count--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName()+"count="+count);
}
}
运行结果
Thread-0count=9
Thread-3count=8
Thread-4count=7
Thread-2count=6
Thread-1count=5
- 使用volatile关键字
- 案例一
public class T_09_Synchronized implements Runnable{
public static void main(String[] args) {
T_09_Synchronized t_09_synchronized = new T_09_Synchronized();
for(int i=0;i<5;i++){
//起五个线程
new Thread(t_09_synchronized).start();
}
}
private volatile int count = 10;
//使用synchronized关键字,保证了线程原子性,多个线程并发操作不影响结果
@Override
public void run() {
count--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println(Thread.currentThread().getName()+"count="+count);
}
}
执行结果
Thread-4count=5
Thread-3count=5
Thread-2count=5
Thread-1count=5
Thread-0count=5
- 案例二
public class T_10_VolatileDemo implements Runnable {
//如果不加volatile永远执行不到end
private volatile boolean running = true;
public static void main(String[] args) throws InterruptedException {
T_10_VolatileDemo t_10_volatileDemo = new T_10_VolatileDemo();
new Thread(t_10_volatileDemo).start();
TimeUnit.SECONDS.sleep(1);
t_10_volatileDemo.running = false;
}
@Override
public void run() {
System.out.println("start");
while (running){
}
System.out.println("end");
}
}
- 执行结果
start
end
好了,介绍完两者之间的区别,相信读者大致有对这两个关键字有所了解,那么接下来我们就来深入的了解下这两者各自的特性
synchronized
- 锁的是对象不是代码
- 锁定方法和非锁定方法可以同时执行
- synchronized保证数据的可见性和原子性
何为synchronized锁的细化?
- 在真正需要使用到锁的地方加上synchronized关键字
//锁的细化
public class FineCoarseLock {
int count = 0;
void run() {
//这里是要处理的业务逻辑,这里使用sleep来代替
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//当只有这里是有共享数据,将锁加载这 而不是加在 synchronized void run()上
synchronized(this){
count++;
}
//这里是要处理的业务逻辑,这里使用sleep来代替
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
何为synchronized锁的粗化?
- 锁的粗话涉及到一个锁升级机制,那么何为锁升级机制
偏向锁
- 线程来之后不加锁,记录线程的id。下次再有线程进来的时候,认为这是上次进来的线程,如果还是上次进来的线程就继续运行不考虑加锁。如果是新的线程不是原来的线程就进行一个锁升级;
自旋锁
- 使用场景(执行时间短,线程少,使用自旋锁)
- 当有线程将对象锁住之后,自旋锁在外面不断徘徊判断是否能拿到这个锁,当自旋十次还拿不到,执行锁升级。
重量级锁
- 使用场景(执行时间长,线程多,使用os锁)
- 经过os进入等待队列里
volatile
- 作用:保证线程的可用性,禁止指令重排序
- 可见性的实现:缓存一致性
- 指令重排序的实现: 添加内存屏障
好了,大概我所知道的synchronized和volatile的区别与各自的有点就总结到这。接下来我们说一说锁的几种机制。
乐观锁(CAS)
- 什么是乐观锁?
简而言之,乐观锁就好比行数。当你期望的值与新值不相同的时候才进行比较。
- 使用乐观锁会产生什么问题吗?
比较典型的就是ABA问题
- 什么是ABA问题?
当多个线程访问时候,当线程A需要修改1为2的时候,此时B线程刚好拿到值把1改成3,C线程又把3改成1,此时A线程就会出现所谓的ABA问题。
- 如何解决ABA问题?
添加版本号或者时间戳。
- 下面我们来举个栗子,具体说一下加不同锁机制的执行效率是怎么样的。
/**
* 比较不同锁的执行效率
*/
public class AtomicIntegerDemo {
/**
* CAS锁
*/
private static AtomicInteger count1 = new AtomicInteger(0);
/**
* 加synchronized 相当于悲观锁,效率低
*/
private static long count2 = 0;
/**
* CAS+分段锁 当执行的线程较大的时候 效率高
*/
private static LongAdder count3 = new LongAdder();
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[1000];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(()->{
for (int k=0;k<100000;k++){
count1.incrementAndGet();
}
});
}
long start = System.currentTimeMillis();
for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
long end = System.currentTimeMillis();
System.out.println("AtomicInteger的值为:"+count1.get()+",消耗的时间为:"+(end-start));
System.out.println("==========================================================");
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(()->{
for (int k=0;k<100000;k++){
synchronized (AtomicIntegerDemo.class){
count2++;
}
}
});
}
start = System.currentTimeMillis();
for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
end = System.currentTimeMillis();
System.out.println("count2的值为:"+count2+",消耗的时间为:"+(end-start));
System.out.println("==========================================================");
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(()->{
for (int k=0;k<100000;k++){
count3.increment();
}
});
}
start = System.currentTimeMillis();
for (Thread t : threads) t.start();
for (Thread t : threads) t.join();
end = System.currentTimeMillis();
System.out.println("count3的值为:"+count2+",消耗的时间为:"+(end-start));
}
}
执行结果
AtomicInteger的值为:100000000,消耗的时间为:1725
==========================================================
count2的值为:100000000,消耗的时间为:1111
==========================================================
count3的值为:100000000,消耗的时间为:615
- 总结
在使用不同的锁机制执行的效率也不相同,不能说一定采用CAS或者分布式锁的效率高,要看具体的场景和数据量.
好了,那么我们来说说其他的锁。想必大家面试的时候面试官不仅仅只会问你一个synchronized。接下来我们介绍下ReentrantLock怎么使用
ReentrantLock
- 说明:
也是一个锁的机制,它与synchronized最大的区别就是需要手动加锁解锁。
- 采用的结构
采用的是CAS的结构
好了,废话不多说我们先上一个案例演示。
public class ReentrantLockDemo {
private ReentrantLock lock = new ReentrantLock();
private int count = 0;
void run() {
for (int i = 0; i < 1000; i++) {
lock.lock();
try {
count++;
} catch (Exception e) {
e.getMessage();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
Thread[] threads = new Thread[30];
for (int i = 0; i < threads.length; i++) {
threads[i]=new Thread(reentrantLockDemo::run);
}
for (Thread thread : threads) thread.start();
for (Thread thread : threads) thread.join();
System.out.println(reentrantLockDemo.count);
}
}
-总结
当我们在使用ReentrantLock锁的时候,不管结果最后都需要将锁释放。
那么一个小小的思考,ReentrantLock还有其他的特性吗?答案当然是有的。那我们就来探究下究竟还有那些特性呢?
公平锁机制
- 添加公平锁机制,使得每个线程进入队列中进行等待。参照代码示例
public class ReentrantLockDemo {
/**如果此处为false说明是非公平锁,那么程序就是谁先抢到资源那么谁就先执行**/
private ReentrantLock lock = new ReentrantLock(false);
void run() {
for (int i = 0; i < 10; i++) {
lock.lock();
try {
//TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+"获得锁.");
} catch (Exception e) {
e.getMessage();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
new Thread(reentrantLockDemo::run).start();
new Thread(reentrantLockDemo::run).start();
}
}
执行结果
Thread-0获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
- 如果是为true的话
public class ReentrantLockDemo {
/**如果此处为true说明是非公平锁,那么程序就是谁先抢到资源那么谁就先执行,只不过相对分配会以先进先出的原则 但也不能保证**/
private ReentrantLock lock = new ReentrantLock(true);
void run() {
for (int i = 0; i < 10; i++) {
lock.lock();
try {
//TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+"获得锁.");
} catch (Exception e) {
e.getMessage();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
new Thread(reentrantLockDemo::run).start();
new Thread(reentrantLockDemo::run).start();
}
}
执行结果
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-0获得锁.
Thread-1获得锁.
Thread-1获得锁.
-
综上所述,执行结果有多条不相同的,所以公平锁和非公平锁其实也是不能保证线程的执行先后的,个人觉得这块了解即可。
-
好的,那么接下来我们开始来说说CountDownLatch
countDownLatch
- 作用是什么?
- 这个类使一个线程等待其他线程各自执行完之后再执行
- 是通过程序计数器来实现的,计数器的初始值是线程的数量。当每个线程开始执行之后,计数器的值就减1,当计算器的值为0的时候,在等待的线程就可以重新开始工作了。
源码:
- countDownLatch类中只提供了一个构造器:
//参数count为计数值
public CountDownLatch(int count) { };
- 类中有三个方法最重要
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };
//将count值减1
public void countDown() { };
那么下面我们就通过具体的案例来说明讲解改如何使用
public class CountDownLatchDemo {
private static ReentrantLock lock = new ReentrantLock();
//定义
private static CountDownLatch countDownLatch = new CountDownLatch(30);
private static int count = 0;
static void run() {
for (int i = 0; i < 100; i++) {
lock.lock();
try {
count++;
} catch (Exception e) {
e.getMessage();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
Thread[] threads = new Thread[30];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
run();
//每次start一个线程 count数量-1
countDownLatch.countDown();
});
}
for (Thread t : threads) {
t.start();
}
try {
//使主线程处于等待状态
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(count);
}
}
执行结果
3000
CyclicBarrier
在介绍它之前我们必须着重清楚什么是CyclicBarrier,它是一个同步辅助类,它允许一组线程相互等待,直到到达某个公共的屏障点。
- 直接使用例子说明
public class CycliBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(20, () -> {
System.out.println("当线程满20个,就放行!");
});
for (int i = 0; i < 100; i++) {
new Thread(()->{
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
ReadWriteLock(读写锁)
在我们的实际应用场景中,当读数据的时候我们应该时共享锁,为写数据的时候我们应该是互斥锁。那么ReadWriteLock就能很好的解决此类问题。
public class ReadWriteLockDemo {
//定义读写锁
static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//定义读锁
static Lock readLock=readWriteLock.readLock();
//定义写锁
static Lock writeLock=readWriteLock.writeLock();
static int value = 0;
public static void main(String[] args) {
for(int i=0 ;i<10;i++){
new Thread(()->{
try {
readLock.lock();
Thread.sleep(1000);
System.out.println("定义读锁");
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}).start();
}
System.out.println("========");
for(int k=0 ;k<10;k++){
new Thread(()->{
try {
writeLock.lock();
Thread.sleep(1000);
System.out.println("定义写锁");
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}).start();
}
}
}
执行结果(读锁一次性全部出来,写锁每隔1S出来一个)
========
定义读锁
定义读锁
定义读锁
定义读锁
定义读锁
定义读锁
定义读锁
定义读锁
定义读锁
定义读锁
定义写锁
定义写锁
定义写锁
定义写锁
定义写锁
定义写锁
定义写锁
定义写锁
定义写锁
定义写锁
LockSupport
LockSuppot是一个线程阻塞的工具类,它的所有方法都是静态方法。可以在线程的任意位置阻塞。
类似于notify/wait 区别在于
notify只能唤醒任意一个线程,但是unpark可以唤醒执行线程
- 案例
public class LockSupportDemo {
public static void main(String[] args) {
Thread thread = new Thread(()->{
for(int i=0;i<10;i++){
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(i);
if(i==6){
LockSupport.park();
}
if(i==8){
LockSupport.park();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
//只能让第一次的LockSupport.park();生效
LockSupport.unpark(thread);
}
}
执行结果 (只能让第一次执行LockSupport.park()生效,后面会阻塞)
Connected to the target VM, address: '127.0.0.1:56743', transport: 'socket'
0
1
2
3
4
5
6
7
8