所处位置
atomic是原子性的意思
Runnable 没有返回值,而且效率相对于Callable来说低一些
进程:一个可以执行的软件跑起来就会开启一个进程,一个进程包含多个线程,至少包含一个
JAVA默认有几个线程
2个,main和GC
Java真的可以开启线程嘛?
不可以,比如start()方法,可以走进去去看源码,首先把当前线程加入到了一个线程组里面去,最后调用一个native()本地的c++方法创建的线程,java无法直接操作硬件
线程:开启了一个QQ,然后QQ跟别人聊天就是一个线程
并发、并行是什么?
并发才存在多线程,多个线程同时操作一个资源,一个CPU
并行是多个线程一起走,多个CPU
/**
* @author :Yan Guang
* @date :Created in 2021/1/17 17:59
* @description: 查看电脑CPU核数
*/
public class Demo {
public static void main(String[] args) {
//cpu密集型、io密集型
System.out.println(Runtime.getRuntime().availableProcessors());
}
}
并发编程的本质:充分利用cpu资源
线程有几个状态?
6个
public enum State {
新生
NEW,
运行
RUNNABLE,
阻塞
BLOCKED,
等待,死死的等
WAITING,
超时等待,只等待一段时间
TIMED_WAITING,
终止
TERMINATED;
}
wait和sleep的区别
1、来自不同的类
wait 来自Object
sleep 来自Thread
2、关于锁的释放
wait会释放锁
sleep会抱着锁,不释放锁
3、使用的范围不同
wait 必须在同步代码块中
sleep 可以在任何地方睡
4、是否需要捕获异常
wait 不需要捕获异常
sleep必须要捕获异常
传统Synchronized
本质:队列+锁 进入代码块
Lock接口
业务代码放到try里面,synchronized可以理解为自动挡,lock就是我们的手动档
三个实现类:
可重入锁,读锁和写锁
公平锁:十分公平:可以先来后到
非公平锁:十分不公平:可以插队(默认)
这里比如一个线程需要执行3H 而另一个只需要3s,所以利用公平锁不合理,默认不使用!
Synchronized和Lock锁的区别?
1、Synchronized 是一个关键字,而Lock是一个java类
2、Synchronized 无法判断获取锁的状态,Lock锁可以判断是否获取到了锁
3、Synchronized 会自动释放锁、Lock需要手动释放锁!如果不释放锁就会死锁
4、Synchronized 线程1(获得锁)、线程2(等待);Lock锁就不会傻傻的等待下去了Lock.tryLock()方法,去尝试获取锁
5、Synchronized 可重入锁,不可以中断的,非公平锁;Lock,可重入锁、可以判断锁、非公平(可以自己设置-通过构造器);
6、Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码
锁是什么?如何判断锁的是谁?
Synchronized + wait + notify
public class TestPC {
public static void main(String[] args) {
hunCun hunCun = new hunCun();
new Productor(hunCun).start();
new Customer(hunCun).start();
}
}
//生产者只顾生产,一直往缓存区里面加鸡
class Productor extends Thread{
hunCun chun;
public Productor (hunCun hunCun){
this.chun=hunCun;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("生产了"+i+"只鸡");
try {
chun.push(new Chicken(i));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者只顾消费,一直往缓存区里面取鸡
class Customer extends Thread{
hunCun chun;
public Customer (hunCun hunCun){
this.chun=hunCun;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
System.out.println("消费了"+chun.pop().no+"只鸡");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Chicken {
int no;
public Chicken(int no) {
this.no = no;
}
}
class hunCun{
Chicken[] chickens = new Chicken[10];
int count =0;
public synchronized void push(Chicken chicken) throws InterruptedException {
if (count==chickens.length){
//通知生产者等待
this.wait();
}
chickens[count]=chicken;
count++;
//如果缓存区没有满,就唤醒生产者继续生产
this.notify();
}
public synchronized Chicken pop() throws InterruptedException {
if (count==0){
//让消费者等待
this.wait();
}
count--;
//通知消费者可以消费了
this.notify();
return chickens[count];
}
}
面试题:单例模式、排序算法、生产者消费者、死锁
问题存在?
当多个线程都是用if判断的话就会出现问题,叫做虚假唤醒问题
改为if while 判断就可以了
这个地方我详细解释一下原因 如何解决虚假唤醒问题!
当消费者给生产者发送信息说可以进行生产的时候,两个生产者线程同时接受到了信息,然后假设生产者A先进入 了线程进行if判断然后生产了数量为1,此时假设总数就为1,就会执行wait()方法,释放锁、然后又进入等待状态,此时生产者B进来了,重新拿锁,但是if只会判断一次,所以生产者B就越过了if判断,直接执行后端生产的业务代码,所以就数量会为2,解决这个办法就是if改为while就可以了,这样就算前面的线程已经进入等待释放锁的情况,后面的线程也还是会接受判断!
Lock类里面
我们发现await()方法是Condition类里面的
所以,如上图可知道,Lock里面可以使用await() 和 signal() 方法(这两个方法是Conditon类里面的)跟wait()和notify()方法是一样的!
但是Condition的优势在哪里呢?
Condition可以精准的通知和唤醒线程!
代码测试:
public class TestPC3 {
public static void main(String[] args) {
Data3 data =new Data3();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.printA();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.printB();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
data.printC();
}
}, "C").start();
}
}
class Data3{
private Lock lock =new ReentrantLock();
//同步监视器
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
private int number =1;
//使用的是同一把锁
public void printA(){
lock.lock();
try {
while (number!=1){
condition1.await();
}
System.out.println(Thread.currentThread().getName()+"A");
//指定唤醒B
number=2;
//通知监视器二,唤醒线程
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try {
while (number!=2){
condition2.await();
}
System.out.println(Thread.currentThread().getName()+"B");
//指定唤醒B
number=3;
//通知监视器三
condition3.signal();
}catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try {
while (number!=3){
condition3.await();
}
System.out.println(Thread.currentThread().getName()+"C");
//指定唤醒B
number=1;
//通知监视器三
condition1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
}
}
这里就是一条生产线的意思!Conditioin就是辅助Lock的同步监视器
如何判断什么是锁?而且锁的是谁?
深刻理解锁
/**
* @author :Yan Guang
* @date :Created in 2021/1/21 19:47
* @description: 八个问题
* 1、标准情况下,两个线程先打印 发短信还是打电话? 答案:发短信
* 2、发短信方法延迟四秒先发短息还是先打电话? 答案:发短信
* 因为我们的代码执行到了第一个线程的时候就把锁给拿住了,锁的调用者来操作锁,
* 此时只有一个对象操作锁,所以也只有一把锁
*/
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sendSms();
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone.call();
}).start();
}
}
class Phone {
public synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
/**
* @author :Yan Guang
* @date :Created in 2021/1/21 19:47
* @description: 八个问题
* 3、增加了一个普通方法之后限制性哪个? 答案:普通方法,因为没加锁,只用等1s就可以执行了
* 4、增加一个对象,是先打电话还是先发短信? 答案:打电话 因为锁的持有者是调用者,而这里的调用者是两个
*/
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
phone.sendSms();
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.hello();
}).start();
}
}
class Phone {
public synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
public void hello(){
System.out.println("打电话");
}
}
/**
* @author :Yan Guang
* @date :Created in 2021/1/21 19:47
* @description: 八个问题
* 5、如果把两个方法都改为static那么先执行哪个? 答案:还是发短信 static是静态方法,类一加载的时候就会执行
* 此时的锁的操作者是Class对象,也是只有一个,但不是phone对象,这里要搞清楚!
*6、把下面的换为普通方法,顺序如何? 答案:打电话,因为不是一个锁
*
*/
public class Test1 {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{
phone.sendSms();
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
phone2.call();
}).start();
}
}
class Phone {
public static synchronized void sendSms(){
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("发短信");
}
public synchronized void call(){
System.out.println("打电话");
}
}
new 出来的是一个手机
static 锁的是Class
public class Demo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
普通的解决方法一:
List是线程不安全的,list.add方法不是一个同步方法,有一个Vector对象可以代替Arraylist,但是Vector是JDK1.0的东西,他里面的add方法加上了sychronized
普通的解决方法二:
所以我们这里还有第二种解法就是通过
List list = Collections.synchronizedList(new ArrayList<>());
这种方式来解决, 因为我们的总容器Collections是可以实现线程安全的方法的,我们只需要调用就可以了
JUC解决方法一:
CopyOnWriteArrayList cow写入时复制,这是计算机的一种优化策略
通过concurrent包下的CopyOnWriteArrayList方法,也可以创建一个线程安全的List
写入时复制意思就是说写入的时候先把数据复制备份一份,然后到后面再读取,这样就不会出现数据读错了的现象了
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
copyonwritearraylist里面的add方法不单单知识加上了重写锁,实际上是把写入的数据首先先全部复制到一个新的容器里面去,然后,复制完之后再存入到集合当中去,之分安全,读写原理!
为什么不用Vector?却要用CopyonwriteArraylist?
因为Vector的add方法仅仅就是加上了一个sychronized而已!
hashset同样也是不安全的
普通的解决方式一:
Collection工具包下的synchronizedSet方法可以给hashset集合的外面再包装一层synchroniezed方法
JUC的解决方式一:
JUC包下面有一个类就叫做CopyonwriteArrayset,直接调用就可以了,里面用的也是跟list一样的方法,读写分离,写入时复制
HashSet底层?
public HashSet() {
map = new HashMap<>();
}
本质就是一个HashMap
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
add方法,实际上就是map里面的键,所以set的值是无法重复的
hashmap为什么不安全?
使用多线程依然会报并发不安全的异常!
解决方法 这里还是在JUC的包下,但是不是跟list跟set一样的copyonwrite 而是叫concurrentHashmap
1、他有返回值
2、可以抛出异常
3、run方法改为call方法
这里补充一个知识点,就是我们的Callable底层实际上还是借助的runnable接口实现的,下面开始分析:
首先runnable接口有一个实现类是futureTask,然后我们进去发现
我们可以在构造中传入一个callable接口,会执行给定的callable,然后callbable执行自己的call方法!
还有一个小点就是futrueTask在执行get方法的时候,就是获取callable的返回值的时候
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
会通过一个状态值,等待1秒钟!
public static void main(String[] args) throws InterruptedException {
//总数是6,进倒计时
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"go out");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
//这里会等待计数器归零之后再执行后面的代码,也就是执行完全部的线程
countDownLatch.await();
System.out.println("关门");
}
解释:
CountDownLatch就是向下计数器的意思,然后有一个countdown方法可以每次将计数器的值减去1,最后用await方法,等待所有的线程执行完,也就是计数器的值为0的时候,才会执行await之后的代码
public static void main(String[] args) throws InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(5,()->{
System.out.println("火箭发射!");
});
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(()->{
System.out.println(Thread.currentThread().getName()+ finalI);
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
这里就是向上的计数器,然后里面的方法也差不多,就是cyclibarriar可以在构造的时候就直接传参过去,填写需要达到计数后的线程执行的操作,也还是用await方法去等待。
semaphore 是信号量的意思
例子:
抢车位
6个车抢3个停车位
//构造器要传入一个线程数量,表示停车位
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
semaphore.acquire();//得到
System.out.println(Thread.currentThread().getName()+"抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
}).start();
}
就是可以理解为一个线程池吧,就是里面一开始定义线程池的上限是多少,然后可以用acquire方法拿到许可证,让那个线程进去,然后线程执行完毕之后,又release释放许可证,然后让其他的线程去拿许可证进入到线程池,可以起到一个限流的作用!
原理:
semaphore的acquire()方法会获取信号量
semaphore的relase()方法会释放信号量
实战我就不演示了,直接给总结:
readwrtielock是一个接口,而且只有一个子类就是reentrantreadwritelock,然后这里有读锁和写锁两个方法,这里的锁就是readlock.lock方法就是读锁(也叫共享锁)方法,writelock就是写锁的方法(也叫独占锁),可以多读,但是只能一个线程一个线程的写,并且,读和写的过程是互斥的
一个队列进入写入操作的时候,如果队列满了就会堵塞,对于读操作来说,如果队列空了,就会阻塞
一般什么情况下我们会使用我们的阻塞队列?
在并发处理和线程池的情况下
BlockinQueue 是在utils包下的Collection接口下的Queue接口里面的,是阻塞队列
而Deque是我们的双端队列,也就是取数据的时候有两个指针从两端都可以去取
AbstractQueue是我们的抽象队列,也叫非阻塞队列
这里的ArrayBlockQueue就是我们的阻塞队列,在构造我们的阻塞队列的时候就需要传入一个固定的队列长度,(因为是阻塞队列,所以肯定是不能扩容的)使用add和remove方法进行存和取就会抛出异常!而offer和poll只会返回一个结果值,但不会报错,不停止执行程序!element是查看队首的方法(抛出异常情况),peek也是检测队首元素的方法(不抛出异常情况下)
阻塞(等待)也分两种情况,一般等待和延时等待
阻塞等待一般不用哈,意思就是说代码会死在阻塞这里一直等待
情况太多了,我搞个表格给大家看一下
这里超时等待的参数就是时间单位和时间,这里面因为是阻塞式的所以所有的方法都是加了锁
超时等待的意思就是线程发现阻塞的情况不会立马报错或者返回值,而是会进行等待一段时间,如果时间到了,线程就会尝试进去,如果依旧还是满的,那么就会报错或者返回值
没有容器,每次只能进去和出来一个element,有阻塞等待的put和take方法,也有超市等待的两个方法offer和poll(因为是超时等待所以有两个o)
里面只会存放一个数据
线程池:三大方法、7大参数、4种拒绝策略
池化技术一句话:备好资源,方便去拿,拿完就还,方便继续取
线程池的好处:
1、降低资源的消耗
2、提高响应的效率(不用创建和销毁)
3、方便管理
一句话:可复用、可控制并发数、可管理线程
三大方法:
newSingleThreadExecutor() 单个线程
newFixedThreadPool() 固定线程(需要构造传参)
newCachedThreadPool() 大小自动伸缩
创建好了线程池之后,execute方法就是执行线程,然后shutdown方法就是关闭线程池
源码对比:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public ThreadPoolExecutor(int corePoolSize, 核心线程数
int maximumPoolSize, 最大线程数
long keepAliveTime, 保持线程活跃的时间
TimeUnit unit, 时间单位
BlockingQueue<Runnable> workQueue, 阻塞队列
ThreadFactory threadFactory, 线程工厂(用于创线程)
RejectedExecutionHandler handler) { 拒绝执行策略
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
Core就是核心线程的意思,任何情况下都是开着的,而我们的Max是人多的时候才会开, 也就是阻塞队列阻塞的时候就会开启,此时再进来线程就会执行我们的拒绝策略!
public ThreadPoolExecutor(int corePoolSize, 核心线程数
int maximumPoolSize, 最大线程数
long keepAliveTime, 保持线程活跃的时间
TimeUnit unit, 时间单位
BlockingQueue<Runnable> workQueue, 阻塞队列
ThreadFactory threadFactory, 线程工厂(用于创线程)
RejectedExecutionHandler handler) { 拒绝执行策略
RejectedExecutionHandler
含有四种拒绝策略:(默认的是AbortPolicy)
AbortPolicy拒绝策略会抛出异常
CallerRunsPolicy拒绝策略就是哪来的去哪里,一般都是main
DiscardOldestPolicy拒绝策略就是不会抛出异常,丢掉任务
DiscardPolicy拒绝策略会尝试去跟第一个线程竞争,失败了依然会丢掉任务
我们也可以自定义通过ThreadPoolExecutor来创建线程池
最大线程到底如何定义?
1、CPU密集型,看CPU是几核的就是几,CPU效率更高
可以通过Runtime.getRuntime().availableProcessors()方法获得
2、IO密集型 ,判断程序中耗费io的线程有多少个即可
只有一个方法的接口==@functionInterface== 简化编程模型,在新版本的框架底层大量应用一句话:用lambda先出一个匿名内部类
有一个输入参数,返回值是boolean类型
只有返回值,没有输入值
只有输入,没有返回值
总结:
一个接口只有一个方法的就叫做函数式接口,可以用匿名类内部类的形式集合lambda表达式写出来,都在util,function包下有这三种接口:返回值是boolean类型的就叫做断定型接口(predicate);只有返回值没有输入值的叫提供者接口(supplier);只有输入值没有返回值的叫做消费者接口(consumer),一定要注意是站在接口的角度去考虑!
什么是forkjoin?
并行执行任务!提高效率,把大任务拆分成小任务
特点:工作窃取
test3的方式是stream的并行流的方式执行,会同时执行你的cpu所以会大大提高效率
Future设计的初衷:可以对将来某个时间的接口进行建模
常用实现类CompletableFuture的常用方法
BiConsume可以传入两个参数的消费者接口
大家可以自行去Future的类去看其它API的使用
请你谈谈对Volatile的理解
Volatile是java虚拟机提供的轻量级的同步机制
1、保证可见性
2、不保证原子性
3、禁止指令重排
什么是JMM?
jmm是java内存模型,是一个约定,不存在的东西
关于jmm的一些同步的约定:
1、线程解锁前,必须把共享变量立刻刷回主存
2、线程加锁前、必须读取主存中的最新值到线程工作内存中!
3、加锁和解锁是同一把锁
线程: 工作内存、主内存
8种操作:
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则:
不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
不允许一个线程将没有assign的数据从工作内存同步回主内存
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
对一个变量进行unlock操作之前,必须把此变量同步回主内存
1、保证可见性
public class Demo {
private volatile static int nums = 0;
//不加上volatile程序就会进入死循环,因为nums=1,没有通知线程Thread说参数改变了
//加上Volatile可以保证可见性
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (nums==0){
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
nums =1;
System.out.println(nums);
}
}
不保证原子性
原子性:不可分割
Lock可以解决原子性,但是Volatile能保证
比如num++被多个线程执行的时候,一定达不到总和,因为num++不是一个原子性操作,汇编被经历了三步,虽然解决了指令重排的问题,但是当线程数量较多的时候,Volatlie会因为被抢占还来不及通知其他线程主存中的资源已经被改变,所以不能保证原子性。
解决方案
使用原子类(JUC.atomic包下)去解决原子操作
可以newu一个AtomicInteger对象
里面有一个getAndIncrement()方法来++
一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作也就是i++,i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写会到缓存中。
线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。
问题来了,线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。
原子类为什么可以这么高级?
这些类的底层都是直接和操作系统操作,在内存中修改值!Unsafe类是一个很特殊的存在
指令重排
什么是指令重排?
你写的程序,计算机并不是按照你写的那样去执行
源代码- > 编译器优化你的重排- >指令并行可能重排->内存系统也会重排->执行
内存屏障,是一个CPU指令。作用:
1、保证特定操作的执行顺序!
2、可以保证某些变量的内存可见性(利用这些特性就可以保证Volatile的可见性)
总结
一句话:Volatile可以保证可见性,不能保证原子性,由于内存屏障,可以保证指令重排的现场产生
内存屏障在单例模式使用的最多!!!
饿汉和 DCL懒汉式
饿汉式:
public class Hungry {
private Hungry(){
}
private final static Hungry HUNGRY =new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
由于饿汉式很饿所以一开始就new出了对象, 因为只能被调用一次,所以构造方法是私有的!
但是每次直接都new了一个对象,可能会浪费空间!!!这里加上了锁,所以是原子性操作,再加上Volatile解决指令重排!!!
DCL懒汉式:
public class Lazy {
private Lazy(){
}
private volatile static Lazy lazy;
//双重检测锁模式、懒汉式单例、DCL懒汉式
public static Lazy getInstance(){
if(lazy==null){
synchronized (Lazy.class){
if (lazy==null){
lazy=new Lazy(); //不是一个原子性操作
/*
* 1、分配内存空间
* 2、执行构造方法
* 3、把对象指向这个空间
* 但是由于指令重拍,可能会出错,如果3先执行就
* 会出错,所以需要加上volatile关键字
* */
}
}
}
return lazy;
}
//单线程下这样写是ok的!
}
单例也是不安全的,所以需要枚举
//enum是什么?本身也是一个class类,可以保证对象一定是唯一的
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
内部内的方式实现
public class neibunei {
private static class Single{
private static final neibunei NEIBUNEI =new neibunei();
}
public neibunei() {
}
public static final neibunei getInstance(){
return Single.NEIBUNEI;
}
}
枚举通过反编译发现最终是还是有参数构造器的
什么是CAS?
大厂你必须要研究底层!所有突破!
public static void main(String[] args) {
//初始化一个值
AtomicInteger atomicInteger=new AtomicInteger(2020);
//CAS :比较并交换
// public final boolean compareAndSet(int expect, int update) {
// return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
// }
//如果我期望的值拿到了就更新,没有达到就不更新,CAS是cpu的并发原语!
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
Unsafe类
unsafe类是java的后面可以操作c++,从而操作内存,objectFieldOffset可以获取内存地址的偏移值
然后我们进入到unsafe类里面去看这个方法,这里就是之前所说的+1原子性的+1方法
这里我解释一下,就是假如你要i++,首先走的就是这个getAndIncrement方法,首先会定义一个变量通过getIntVoliatle来获取并且暂存你的地址偏移值(Volatile可以解决可见性问题和避免指令重排),然后通过CAS判断,compareAndSwap如果地址值依旧是期望值,那么就++,这样就可以保证原子性问题了。
CAS:一句话比较当前工作内存中的值和主存中的值,如果是这个期望值就执行操作,不是就一直循环! 在内存中看的是地址偏移值
缺点:
1、循环会耗时
2、一次性只能保证一个共享变量的原子性
3、ABA问题
CAS:ABA是什么?(狸猫换太子)
ABA问题就是:假如说两条线程同时在操作一个数据A的时候,线程2经过CAS操作已经把期望值修改过一遍了,此时的线程一所看到的值并不是原来的值,已经狸猫换太子了
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2021, 2020));
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(2020, 2021));
System.out.println(atomicInteger.get());
对于我们平时写的Sql:乐观锁!
乐观锁就是他认为别人都没改过!
悲观锁就是他认为别人都改过了!
对应的思想:乐观锁
AtoReferencen是原子引用的意思
解决ABA问题就用AtomicStampedReference加一个版本号就可以了,我们就知道被修改过了
int stamp = atomicInteger.getStamp();//获得版本号
然后再CompareAndSet里面就可以输入版本号参数
1、公平锁、非公平锁
公平锁:非常公平,线程不能插队
非公平锁:线程可以插队ReentrantLock默认就是非公平锁,构造器加一个true就可以改为公平锁
2、可重入锁、递归锁
就是锁里面可以还有锁
这里执行一个线程调用sms方法的话会输出sms和call之后分别释放两个锁,这种锁就叫可重入锁
Lock1-Lock2-unLock2-unLock2就是执行过程
3、自旋锁(SpinLock)
public class SpinLock {
AtomicReference<Thread> atomicReference=new AtomicReference<>();
public void myLock(){
Thread thread = Thread.currentThread();
//自旋锁
while (!atomicReference.compareAndSet(null,thread)){
}
}
public void myUnlock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
}
}
如果没有达到期望值就会一直循环,一直保持锁的情况
这里利用的while循环来代表阻塞的意思,假如一个线程想要获取的锁没有得到的话就一直处于循环状态就叫自旋锁!
4、死锁
死锁是什么?
死锁产生的四个必要条件(也叫后果):互斥,循环等待,占有等待,不可抢夺
互斥就是资源互斥,只能一个线程拥有;循环等待就是线程会成为一个回路,怎么也走不通;占有等待就是资源被一个线程占有了,当次线程处于阻塞状态的时候,不放,始终占有;不可抢夺就是在资源没有释放之前,其他线程不可抢夺资源
public class Demo {
public static void main(String[] args) {
new Thread(new MyThread("a","b"),"1").start();
new Thread(new MyThread("b","a"),"2").start();
}
}
class MyThread implements Runnable{
//因为这里的两个资源会被放进常量池中获取,所以就算创建了两个对象实际上也只有两个-
String a = "a";
String b = "b";
public MyThread(String a, String b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
synchronized (a){
System.out.println(Thread.currentThread().getName()+a);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println(Thread.currentThread().getName()+b);
}
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread myThread = new MyThread(true);
new Thread(myThread,"1").start();
new Thread(myThread,"2").start();
}
}
class MyThread implements Runnable{
//因为这里的两个资源会被放进常量池中获取,所以就算创建了两个对象实际上也只有两个
String a = "a";
String b = "b";
boolean flag = true;
public MyThread(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
//为第二个线程把flag设置为false
flag=false;
synchronized (a) {
System.out.println(Thread.currentThread().getName() + a);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println(Thread.currentThread().getName() + b);
}
}
}
if (!flag) {
flag=true;
synchronized (b) {
System.out.println(Thread.currentThread().getName() + b);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a) {
System.out.println(Thread.currentThread().getName() + a);
}
}
}
}
}
可以用jps -l看现在java活着的进程
用jstack查看进程信息
自行测试~完结撒花!!!