本文共 12151 字,大约阅读时间需要 40 分钟。
当一个线程必须拿到资源的锁,才进行进行相关操作,否则进入阻塞,直到其他线程释放资源锁;
当一个线程进行操作时,不对资源进行加锁,它认为该对象在当前操作时,应该不会有其他线程来影响,所以多个线程都可以对该资源进行操作,对操作进行提交之前,会进行一次比较,把该资源和初始资源进行对比,如果资源内容一致,那么认为这个资源当前只有我操作了,那么就直接提交资源修改;否则进行重试或报错等;
1)原子性:在cpu执行时间单位内,一个操作要么全部执行完成,不可被中断,中断会出现线程安全问题;
2)可见性:一个线程修改的内容能够被其他线程所读取,换句话说,如果一个对象是可见的,那么多线程之间修改对象,都是互相知晓的,保证对象在多个线程之间读取到最新的值;
3)有序性:在Java内存模型中,允许编译器和处理器对指令进行重排序,重排序会影响到多线程并发执行代码的正确性;
synchronized是jvm提供的关键字,他的作用是为了保证操作的原子性,也加加锁;
这个锁是一种悲观锁,同一个对象锁,同一时刻只能被一个线程锁占有,也叫独占锁,也叫可重入锁; 它实现原子性的原理是:利用底层的Monitor对象锁,加锁monitorEnter,释放锁monitorExit; 竞争激烈的情况下,使用它;volatile关键字也是jvm提供的关键字,他的作用是为了保证多线程之间的可见性和一致性;
他不是锁,他是为了保证被volatile修饰的属性在多线程之间的访问安全性;一般用在判断true,false场景;他不具备有原子性,比如在循环i++场景,无法保证i最终得到正确的值;Atomic保证对象操作的原子性,如果一个变量被Atomic修饰,那么该变量在多线程操作之间的修改是安全的;它实现原子性的原理是:利用cas操作,是非阻塞的;竞争不激烈的情况下,使用它;
锁是一种概念,为了保证多线程之间操作对象的安全性;在java中体现为接口Lock;
常用的锁比如:private final ReentrantLock mainLock = new ReentrantLock();
它是一种可重入锁,代表一个线程可以无限对同一个对象锁进行拿锁操作;
一般使用方式为: 定义一个final 全局对象:private final ReentrantLock mainLock = new ReentrantLock();
然后再业务逻辑代码中:
final ReentrantLock mainLock = this.mainLock;mainLock.lock();try { //doBusi}finally { mainLock.unlock();}
该方法为Thread的一个方法,用于设置调用线程的中断标识;意思就是:如果线程调用了该方法,那么该线程可能会中断,也可能不会中断,它仅仅只是设置线程中断的标识等于true,什么时候中断取决于程序本身;
在线程处于等待状态,比如调用了wait,join,sleep等方法之后,如果调用线程的interrupt()方法,那么会抛出一个线程中断异常:InterruptedException,可以通过捕获该异常来终止等待中的线程,也可以调用Thread.interrupted()方法清除中断标识,来忽略该中断请求;
该方法为线程池的关闭操作;如果线程池调用了该方法,且阻塞队列不为空,那么线程池会在阻塞队列任务全部执行完成之后,才会关闭线程池;
该方法一是线程池的关闭操作;如果线程池调用了该方法,会立即中断所有线程池内的线程执行,并且将未执行的任务放入一个list,作为返回,返回类型为任务Runnable
1)原子性
2)一致性 3)隔离性 4)持久性1)newFixedThreadPool:核心线程数等于最大线程数,当线程数已满,任务队列已满,不会继续创建新线程,直接执行拒绝策略;同时阻塞队列为LinkedBlockingQueue,为无限大小队列,风险是:如果阻塞队列过大,可能会造成程序OOM异常
2)newSingleThreadExecutor:线程池内永远只会有一个单线程在执行子任务,阻塞队列为LinkedBlockingQueue,为无限大小队列,风险是:如果阻塞队列过大,可能会造成程序OOM异常
3)newScheduledThreadPool和newSingleScheduledThreadPool:定时线程池,类似一个job定期执行任务,阻塞队列为:DelayedWorkQueue,风险是:如果阻塞队列过大,可能会造成程序OOM异常
4)newCachedThreadPool:缓存线程池,基于任务创建线程,也就是说来多少子任务,就新创建多少线程,如果线程没有被销毁,那么就复用现有线程;阻塞队列为:SynchronousQueue,无法存储任务;风险是:线程创建过大可能会将cpu资源耗尽,因为一个服务器能创建的最大线程数是有上限的;
按照线程先来后到的原则,去依次按照顺序获取锁
在一定条件下,新来的线程可以插队,不需要进入等待队列;
一定条件指的是:当上个线程执行完成,释放锁的一瞬间,刚好来了一个新的线程的拿锁请求,那么cpu会把锁优先分配给它,而不会给等待队列中时间最长的那个;java默认的锁是非公平锁;
非公平锁的好处是:可以优先把锁分配给新来的线程,节省了去唤醒等待队列中的线程的开销; 而且,如果新线程如果执行很快结束释放了锁,相当于既完成了新线程的任务,同时又没有耽误等待队列正在唤醒的线程拿到锁; 缺点是:可能有的等待线程一直拿不到锁出现饥饿;另外,即使设置了锁为公平锁,如果程序中调用的是tryLock(),那么tryLock可以插队,它实际上调用的还是Sync的非公平锁
如果使用ReentrantLock,在只有读没有写的情况下,其实就会造成读性能的影响,因为多线程的读是没有线程安全问题的;
接口ReadWriteLock有个实现类叫ReentrantReadWriteLock,他有2个静态成员:1:读锁对象;2:写锁对象 它允许读锁被多个线程加锁,写锁它只能被一个线程拿锁,其他线程进入等待; 总结来看就是:读读可以同时拿锁,一个线程已经拿了读,另一个想要拿写,必须等上个读释放;同理,一个线程已经拿了写,另一个想要拿读,必须等上个写释放; 代码示例:package com.test;import java.util.concurrent.locks.ReentrantReadWriteLock;public class Test { private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock(); private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock(); private static void read(){ readLock.lock(); try{ System.out.println(Thread.currentThread().getName()+"拿到读锁,正在读取..."); Thread.sleep(600); }catch (Exception e){ e.printStackTrace(); }finally { readLock.unlock(); } } private static void write(){ writeLock.lock(); try{ System.out.println(Thread.currentThread().getName()+"拿到写锁,正在写数据..."); Thread.sleep(600); }catch (Exception e){ e.printStackTrace(); }finally { writeLock.unlock(); } } public static void main(String[] args) { new Thread(Test::read).start(); new Thread(Test::read).start(); new Thread(Test::write).start(); new Thread(Test::write).start(); }}
运行结果:
Thread-0拿到读锁,正在读取...Thread-1拿到读锁,正在读取...Thread-2拿到写锁,正在写数据...Thread-3拿到写锁,正在写数据...可以看出来:线程0和1可以同时读,线程2和3的写只能一个个来
另外:如果设置公平锁=true,那么readLock和writeLock都会排队
如果设置公平锁=false,那么writeLock允许插队,但是readLock不允许插队,他它优先让等待队列的写锁拿锁执行,然后再让新来的线程拿读锁进行读取;
读写锁降级:读写锁允许从写锁降级成读锁,不允许从读锁升级为写锁,因为如果2个读都想要升级成写,那都需要互相等待释放对方的读,造成死锁问题;
jvm的锁升级过程为:
无锁->偏向锁->轻量级锁->重量级锁 偏向锁:开销很小,当对象被尝试拿锁时,会记录该对象信息,下次拿锁,直接上锁; 轻量级锁:当存在短时间竞争时,偏向锁升级成轻量级锁,利用了自旋和cas; 重量级锁:悲观锁,当锁被其他线程拥有时,当前线程进入阻塞;1:当发生写数据的时候,将当前数组复制出一份新的数组,数组大小为原有数组+1,并且将新元素添加到新数组当中,然后再将旧数组的指针指向新的数组;
2:迭代期间允许修改元素,不会报错,因为修改的是新数组 3:get不加锁,保证多线程访问高效性缺点:
1:因为复制数组,所以多出了一部分内存的开销 2:数据可能会有可见性的问题1.抛出异常
1)add:往队列中添加一个元素,如果队列已满,则抛出异常; 2)remove:删除并返回队列中的元素,如果队列为空,则抛出异常; 3)element:返回队列中的头结点但不删除,如果队列为空,则抛出异常;2.不抛出异常,给出提示
1)offer:往队列插入一个元素,插入成功,返回true,如果队列已满,返回false; 2)poll:删除并返回队列中的元素,如果队列为空,则返回null(前提是你不能往队列中插入null元素),否则无法区分; 3)peek:返回队列中的头结点但不删除,如果队列为空,则返回null;3.put和take
1)put:往队列中插入一个元素,如果队列已满,则线程进入阻塞;如果队列有了空闲,则会将元素添加到队列中; 2)take:获取并移除队列中的头结点,如果队列为空,也就是没有元素可以取出,那么线程进入阻塞,直到队列中有了新元素可以取出;代码示例:
package com.test;import java.util.concurrent.ArrayBlockingQueue;import java.util.concurrent.BlockingQueue;public class Test2 { /*** * 使用BlockingQueue实现生产者和消费者 */ public static void main(String[] args) { //定义一个容量为10的阻塞队列 BlockingQueue
代码示例:
(基于Condition手动实现put和take)package com.test;import java.util.LinkedList;import java.util.Queue;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.ReentrantLock;public class MyBlockingQueue { private Queue queue; private int max = 16; //锁 private ReentrantLock reentrantLock = new ReentrantLock(); //队列非空 private Condition notEmpty = reentrantLock.newCondition(); //队列非满 private Condition notFull = reentrantLock.newCondition(); public MyBlockingQueue(int size) { this.max = size; this.queue = new LinkedList(); } //生产者 public void put(Object o) throws InterruptedException { reentrantLock.lock(); try{ //队列已满,则等待 while (queue.size() == max){ notFull.await(); } //否则新增元素 queue.add(o); //唤醒等待的消费者可以消费元素了 notFull.signalAll(); }finally { reentrantLock.unlock(); } } //消费者 public Object take() throws InterruptedException { reentrantLock.lock(); try{ //如果队列为空则等待,这里不能用if,以为如果2个线程同时消费,那么第一个消费释放锁, //第二个再去remove,因为队列为空,则会抛出异常 while(queue.size()==0){ notEmpty.await(); } //否则进行消费 Object o = queue.remove(); //唤醒等待的生产者可以放入元素了 return o; }finally { reentrantLock.unlock(); } } public static void main(String[] args) throws InterruptedException { MyBlockingQueue queue = new MyBlockingQueue(16); new Thread(()-> { try { while (true){ Object o = new Object(); queue.put(o); System.out.println(Thread.currentThread().getName()+"生产了一个对象"+o); } } catch (InterruptedException e) { e.printStackTrace(); } }).start(); new Thread(()->{ try { while (true){ Object take = queue.take(); System.out.println(Thread.currentThread().getName()+"消费了一个对象"+take); } } catch (InterruptedException e) { e.printStackTrace(); } }).start(); }}
在代码中体现为:
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
这段代码的意思是:将变量进行进行修改,修改提交之前进行比较变量的偏移量和当前的值做比较,如果是同一个值,说明没有被其他线程修改过,那么就提交修改,返回成功,如果返回失败了,怎么办呢?
1)根据实际业务,可以选择放弃本次操作或者认为进行业务重试 2)利用死循环进行自旋cas直到成功为止,代码中一般是这样的:do { } while (! compareAndDecrementWorkerCount(ctl.get()));
注意:cas操作是不可中断的,它是乐观锁的一种是先,它是非阻塞的\
模拟cas执行过程:
package com.test;public class DebugCas implements Runnable{ private volatile int value; public synchronized int compareAndSwap(int expectedValue,int newValue){ int oldValue = value; if(oldValue == expectedValue){ value = newValue; System.out.println(Thread.currentThread().getName()+"更新了value值"); } return oldValue; } @Override public void run() { compareAndSwap(100,150); } public static void main(String[] args) throws InterruptedException { DebugCas r = new DebugCas(); r.value = 100; Thread t1 = new Thread(r,"Thread1"); Thread t2 = new Thread(r,"Thread2"); t1.start(); t2.start(); t1.join(); t2.join(); }}
两个都是原子性操作类
1)AtomicLoing一般用在一般场景,用来保证cas操作; 2)LongAdder一般用在只用来求和和计数的场景,吞吐量较高,性能较高,但是占用内存也较高;代码示例:
package com.test;public class DealLockDemo { private static Object o1 = new Object(); private static Object o2 = new Object(); public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (o1){ System.out.println(Thread.currentThread().getName()+"获取到了o1锁"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o2){ System.out.println(Thread.currentThread().getName()+"获取到了2把锁"); } } },"Thread-1"); Thread t2 = new Thread(() -> { synchronized (o2){ System.out.println(Thread.currentThread().getName()+"获取到了o2锁"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (o1){ System.out.println(Thread.currentThread().getName()+"获取到了2把锁"); } } },"Thread-2"); t1.start(); t2.start(); }}线程1拿到了o1,等待拿o2,线程2拿到了o2,等待拿o1,互相等待,不会释放,形成死锁;
查看jps找到死锁进程pid,然后jstack pid可以看到死锁日志:
Java stack information for the threads listed above:==================================================="Thread-2": at com.test.DealLockDemo.lambda$main$1(DealLockDemo.java:32) - waiting to lock <0x000000076b91acb8> (a java.lang.Object) - locked <0x000000076b91acc8> (a java.lang.Object) at com.test.DealLockDemo$$Lambda$2/122883338.run(Unknown Source) at java.lang.Thread.run(Thread.java:748)"Thread-1": at com.test.DealLockDemo.lambda$main$0(DealLockDemo.java:18) - waiting to lock <0x000000076b91acc8> (a java.lang.Object) - locked <0x000000076b91acb8> (a java.lang.Object) at com.test.DealLockDemo$$Lambda$1/1534030866.run(Unknown Source) at java.lang.Thread.run(Thread.java:748)Found 1 deadlock.
转载地址:http://xikmi.baihongyu.com/