多线程基础
一个Java程序实际上是一个JVM进程,
JVM进程用一个主线程来执行main()
方法,在main()
方法内部,我们又可以启动多个线程。
此外,JVM还有负责垃圾回收的其他工作线程等。
内存角度:单线程相当于栈空间里的函数压栈、串行运行;多线程是每个线程开辟一个栈空间,CPU给多个线程并发运行。
什么是JUC
jdk包的三个首字母Java Util Concurrent
准备工作:
保证IDEA设置:
1)File=>Project Structure=>Modules + Project这两个都是java-8版本
2)File=>Setting=>Build=>Compiler=>Java Compiler是java-8版本
多线程状态&转换:
sleep 休眠
模拟网络延时、倒计时(单位是毫秒ms)
TimeUnit.SECONDS.sleep(5); //休眠
yield 礼让
运行态==>就绪态
就绪态回到运行态后,会继续执行run()方法后面的内容
join 强制执行(插队)
interrupt 中断(他杀)
thread.interrupt(); //中断线程是一个线程,让另一个thread中断
wait(配合notify)
wait必须在同步代码块中(可结合线程状态图来理解)
(sleep可以在任何地方)
jvm中的runnable相当于上图中操作系统状态的running+runnable
统称为阻塞状态三种:它们分别是 Blocked(同步锁定阻塞)
、Waiting(等待)
、Timed Waiting(计时等待)
.
JVM 6状态:
1) 观察线程状态(6种)
2)获取线程名字:
3)
创建 多线程(3种方法):
三种方法创建线程:
1. 继承Thread类,重写run()方法(不推荐,因为java单继承,如果继承Thread就占用了继承的名额)
这时候main的线程和TestThread1的线程并发执行
2. 实现Runnable接口(无返回值)
3. Callable和Future submit接口(有返回值)
例子:
CompleteFuture(Future的优化版):
Callable
callable和runnable的区别:
可以有返回值、可以抛出异常
方法不同:run() 和 call()
线程优先级 (大的优先)
后台线程 daemon (也叫守护线程)
实例:
锁
同步锁 Synchronized
1. 同步方法:临界资源 方法名前 加上"synchronized"关键字;加锁对象默认是this
2. 同步代码块:创建同步的块,将临界资源及其操作放到里面:
只要在方法前加上一个关键词就好了
public synchronized void sell(){
volatile 和 Synchronized 区别
作用级别:volatile 只用于变量; 变量、方法、类
可见性&原子性:volatile可见性,不保证原子性;synchronized 修改可见性、原子性
阻塞:volatile不阻塞;synchronized 阻塞。
编译器优化:volatile不会优化(内存屏障);synchronized可以优化
Lock锁
ReentrantLock 可重入锁 (re entrant lock)
Lock锁示例:
Lock lock = new ReentrantLock();
public void sell(){
lock.lock();
try {
if(number>0){ //限制票数大于0
System.out.println(Thread.currentThread().getName() + "卖出了一张票,还剩下:" + (number--) + "张票。");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} //lock锁必须成对( 一个lock() + 一个unlock() , 不然会死锁 )
ReadWriteLock 读锁+写锁 (ReadLock、WriteLock;读锁==共享锁;写锁==独占锁)
例子:
读锁、写锁分别使用:
Lock和Synchronized的区别:
相同点:
都是可重入锁
区别:
lock是显式锁(手动开关锁); synchronized隐式锁(出了作用域自动释放)
lock只有代码块锁, synchronized有方法锁、代码块锁
lock锁,JVM调度线程花费小、性能好。并且扩展性好(提供更多子类)
优先使用顺序:lock>synchronized代码块>synchronized方法
乐观锁、悲观锁
乐观锁:
假定不发生冲突,提交时,检查版本、回退。
CAS(自旋锁:达到预期就改,否则自旋等待)
ABA问题(A=>B=>A)解决方法用版本机制,AtomicInteger.getStamp()
悲观锁:
假定发生冲突,用锁把事务锁起来:
代码块加锁synchronized
MySQL排它锁(写锁、X锁)
2)公平锁、非公平锁
公平锁:不能插队
非公平锁(默认):可以插队
3)可重入锁 ReentrantLock
可重入锁也叫:递归锁
=> 拿到外面的锁,就自动获得里面的锁。
4)自旋锁 SpinLock
=> 不断循环尝试,直到成功为止
5)死锁排查
遇到死锁(不会爆异常),Terminal命令行操作:
1)使用 jps -l 来定位进程号:
=> 可以找到死锁对应的进程号
2)使用 jstack + 进程号 来查看具体信息:
进程和线程
区别
根本区别:
进程是操作系统 资源分配 的基本单位,而线程是处理器任务 调度和执行 的基本单位
资源开销:
每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:
如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:
同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
影响关系:
一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:
每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行
多个线程共享进程的:堆 + 方法区资源,
每个线程有自己的:程序计数器 + 虚拟机栈 + 本地方法栈
进程:QQ.exe Music.exe 有java环境的.jar
java默认有2个线程:main、GC垃圾回收
开线程方式:Thread(继承)、Runnable(实现接口)、Callable(实现接口+有返回值)
java无法直接开线程,start() 要调用native本地方法,用底层的C++来操作硬件
System.out.println(Runtime.getRuntime().availableProcessors()); //获取CPU核数
并发编程的目的:充分利用多核CPU的资源
线程间通信
1)wait + notify 生产者消费者:
- wait ()
- notify() /notifyAll(
ps: notifyAll 更常用
ps: wait()还有种用法就是wait(1000)这样的加上时间参数,在等待时间结束之后,就不等notify()了
2)join() 方法==>插队
使用场景:
主线程创建并启动子线程,如果子线程中进行大量的运算,主线程往往早于子线程结束。这时主线程要等待子线程完成之后再结束。
比如子线程处理一个数据,主线程要取得这个数据中的值,就要用到join()方法。
join()方法就是等待线程对象销毁。
join的实现其实是基于等待通知机制(wait+notify)。
3)Volatile (利用 可见性)内存共享
- 每次修改变量后,立刻回写到主内存。
- 在此过程中,会通知其他线程读取新值。
4) 管道通信
5)三个常用的辅助类=》计数(CountDownLatch、CyclicBarrier、Semaphore)
(1)CountDownLatch 用来倒数
(2)CyclicBarrier 线程计数器
(3)Semaphore 信号量
进程间通信
套接字(socket)网络通信
消息队列(messagequeue)
管道(pipe)
半双工
管道分为pipe(无名管道)和fifo(命名管道)两种,
pipe:父子进程
信号量(semaphore) ==》PV操作(同步、互斥)
共享内存(shared memory)
生产者消费者
例子:2个线程交替执行
/** * 线程之间通信问题,线程交替执行 * wait notify * */ public class Test { public static void main(String[] args) { MyData myData = new MyData(); //新建对象 new Thread(()->{ for(int i=0;i<10;i++){ try { myData.increment(); } catch (Exception e) { e.printStackTrace(); } } },"product").start(); new Thread(()->{ for(int i=0;i<10;i++){ try { myData.increment(); } catch (Exception e) { e.printStackTrace(); } } },"product-2").start(); new Thread(()->{ for(int i=0;i<10;i++){ try { myData.decrement(); } catch (Exception e) { e.printStackTrace(); } } },"consumer").start(); new Thread(()->{ for(int i=0;i<10;i++){ try { myData.decrement(); } catch (Exception e) { e.printStackTrace(); } } },"consumer-2").start(); } } class MyData{//数字 资源类 private int number =0; public synchronized void increment() throws InterruptedException { while(number != 0)this.wait(); //wait要放在while中 //不能用if,if会虚假唤醒 number++; System.out.println(Thread.currentThread().getName()+ "=>" + "number = " + number); this.notifyAll(); } public synchronized void decrement() throws InterruptedException { while(number == 0)this.wait(); number--; System.out.println(Thread.currentThread().getName() + "=>" + "number = " + number); this.notifyAll(); } }
JUC版 生产者消费者(Condition + lock)
Synchronized => Lock
wait + notifyAll => Condition (await + signal)
1)Condition + lock 替代原有锁的功能
class MyNewData{//数字 资源类 private int number =0; Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); public void increment() throws InterruptedException { lock.lock(); try { while(number != 0)condition.await(); //wait要放在while中 //不能用if,if会虚假唤醒 number++; System.out.println(Thread.currentThread().getName() + "=>" + "number = " + number); condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } lock.unlock(); } public synchronized void decrement() throws InterruptedException { lock.lock(); try { while(number == 0)condition.await(); number--; System.out.println(Thread.currentThread().getName() + "=>" + "number = " + number); condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } lock.unlock(); } }
2)Condition + lock 精准通知唤醒
例子:3个线程循环
package ProducerConsumer; import lombok.Data; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author 晋青杨 j50016344 * @create 2021-04-26 **/ public class Test_3 { public static void main(String[] args) { Data3 data3 = new Data3(); new Thread(()->{ try { for(int i =0;i<10;i++)data3.printA(); } catch (InterruptedException e) { e.printStackTrace(); } },"Thread_A").start(); new Thread(()->{ try { for(int i =0;i<10;i++)data3.printB(); } catch (InterruptedException e) { e.printStackTrace(); } },"Thread_B").start(); new Thread(()->{ try { for(int i =0;i<10;i++)data3.printC(); } catch (InterruptedException e) { e.printStackTrace(); } },"Thread_C").start(); } } @Data class Data3{ private Lock lock = new ReentrantLock(); Condition condition1 = lock.newCondition(); //Alt+Enter自动生成等号左边 Condition condition2 = lock.newCondition(); Condition condition3 = lock.newCondition(); //【重点】 建立3个condition,每个都具有自己的await和signal,可以精准的等待、唤醒; private int number =1; //1A 2B 3C public void printA() throws InterruptedException { lock.lock(); try { while(number!=1)condition1.await(); //1等待 System.out.println(Thread.currentThread().getName() + "=> A"); //唤醒2: number =2; condition2.signal(); } catch (InterruptedException e) { e.printStackTrace(); } lock.unlock(); } public void printB() throws InterruptedException { lock.lock(); try { while(number!=2)condition2.await(); System.out.println(Thread.currentThread().getName() + "=> B"); //唤醒3: number =3; condition3.signal(); } catch (InterruptedException e) { e.printStackTrace(); } lock.unlock(); } public void printC() throws InterruptedException { lock.lock(); try { while(number!=3)condition3.await(); System.out.println(Thread.currentThread().getName() + "=> C"); //唤醒1: number =1; condition1.signal(); } catch (InterruptedException e) { e.printStackTrace(); } lock.unlock(); } }
8锁问题
/**
* ====== 8锁:关于锁的8个问题 ======
* 1) 在中间有1s间隔情况下,两个线程是先 send 还是 call ? 先send => 因为先在synchronized那里抢到锁,就会先执行 => send第0s call第1s
* 2) send在1)的基础上,方法内部追加3s的延迟,两个线程是先 send 还是 call ? 先send => 补充:send和call都是在第3s执行
* 3) 新定义一个不加锁的方法:hello,然后线程B来调用此方法。=> 先hello,再send => hello在第一秒运行到,此时也不用抢锁,直接执行
* 4) 如果两个实例对象,就会有两个不同的锁;两个对象互不影响
* 5) 6)将类中的方法用static修饰,于是锁的是类,于是两个实例对象也会共用一把锁
* 7) 8)一个方法是 静态加锁; 另一个是 普通加锁 => 用的是两个锁,只有静态的方法才会参与类加锁
* 上面的 5)7)是一个实例对象; 6)8)是两个实例对象
* */
public class lock8 {
public static void main(String[] args) throws InterruptedException {
// Phone phone1 = new Phone();
// Phone phone2 = new Phone();
Phone phone = new Phone();
new Thread(()->{
try {
phone.sendMessage();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"Thread_A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
phone.hello();
},"Thread_B").start();
}
}
class Phone{
// synchronized锁的对象是:方法的调用者 => 调用者:普通情况下是对象,加static后是类
public static synchronized void sendMessage() throws InterruptedException {
TimeUnit.SECONDS.sleep(2); //由于这里会等2s所以:如果共用一把锁,则send会先执行,否则别的方法先执行
System.out.println("send message");
}
public synchronized void call(){
System.out.println("call");
}
//没有锁的方法:
public void hello(){
System.out.println("hello");
}
}
集合类不安全
Java STL内置的线程安全的Concurrent Collection:
Atomic原子操作的封装类
getAndIncrement() //num++
IncrementAndGet() //++num
-
原子操作实现了无锁的线程安全;
-
适用于计数器,累加器等。
AtomicInteger
利用 CAS (compare and set) + volatile 来保证原子操作,
从而避免 synchronized 的高开销,执行效率大为提升。
UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。
另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
CAS(CompareAndSet,比较并更新)
=> 如果期望的expect值达到了,就set;否则就不更新,并一直循环等(自旋锁)
CAS缺点:
1、循环会耗时
2、一次性只能保证一个共享变量的原子性
3、ABA问题
原子引用 解决ABA问题
ABA问题就是:A->B->A,其他线程以为没有变化,但实际上是改变过了的
解决方法就是用时间戳来判断有没有被动过
CAS比较并更新的对象是:
值+stamp 2个内容
ConcurrentHashMap原理
HashMap:
Map<String, String> map = new HashMap<String, String>(16, 0.75f); // HashMap 加载因子,初始容量
ConcurrentHashMap并发原理:
jdk1.7:
Segment
+ HashEntry 数据结构,对
Segment加锁;各个
Segment之间互不影响
jdk1.8:
数组 +(链表 / 红黑树),对链表头结点 / 红黑树根节点 加锁;
各个(链表 / 红黑树)之间互不影响
CAS + synchronized
实现更加细粒度的锁
JDK1.8 中为什么使用内置锁 synchronized替换 可重入锁 ReentrantLock?★★★★★
- 在 JDK1.6 中,对 synchronized 锁的实现引入了大量的优化,并且 synchronized 有多种锁状态,会从无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁一步步转换。
- 减少内存开销 。假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,这无疑带来了巨大内存浪费。
读写锁 ReadWriteLock
读写分离,读可以并发,写只能用一个线程。
阻塞队列 BlockingQueue
BlockingQueue是Collection下和Set、List平级的类
运用:多线程并发处理,线程池
线程池
优点:
- 避免重复创建、销毁线程;提高性能
- 便于线程管理(可以控制最大并发数、时间等)
线程池必会:3大方法、7大参数、4种拒绝策略
三大线程池:
SingleThreadExecutor:单线程池
FixedThreadPool:线程数固定的线程池
CachedThreadPool:线程数动态调整
=> 3种方法只是封了一层,使得输入的参数减少了。源码中都是通过ThreadPoolExecutor来产生的线程池,只是3种之间的参数不一样。
7大参数:(3个核心参数)
(建议用 ThreadPoolExecutor 7大参数来自定义,而不是 Executors 去创建,否则可能会导致 OOM。)
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小;
int maximumPoolSize, //最大线程池大小。定义策略:CPU密集型=>几核就定义几个(如12个)
long keepAliveTime, //超时没有使用 就会释放
TimeUnit unit, //超时 时间单位
BlockingQueue<Runnable> workQueue, //阻塞队列(含同步队列)
ThreadFactory threadFactory, //线程工厂:固定用于创建线程
RejectedExecutionHandler handler) { //拒绝策略:超过maximumPoolSize之后的拒绝策略 //7大参数
如果:需要的线程数 > 最大线程池(获取到线程的个数) + 阻塞队列(排队中),就会触发拒绝策略
4种拒绝策略:
Abort【默认】:超出线程,会抛出异常
CallerRuns:超出线程,会还给main或者调用它的地方
Discard:直接丢掉多余的任务,不抛异常
DiscardOldest:尝试与最早使用线程池的线程竞争,而不是直接丢掉。也不会抛异常
最大线程池大小,定义策略(调优):
1)CPU密集型 =>几核就定义几个(如12个)
int coreNum = Runtime.getRuntime().availableProcessors(); //因为不同的电脑不一样,通过此方法动态获取CPU核数
System.out.println("coreNum = " + coreNum);
2)IO密集型 => 比如有15个大型io密集任务,就开辟大于任务数的线程数
设置线程池大小maximumPoolSize为 约2倍 的任务数即可。
Java8新特性
四大 函数式接口(重点)
函数式接口 Function、断定型接口 Predicate、消费型接口 Consumer、供给型接口 Supplier
1)函数型接口Function:只有一个方法的接口
典型例子:
@FunctionalInterface public interface Runnable { public abstract void run(); }
简化编程模型,在新版本的框架底层大量应用
函数式编程 例子:
import java.util.function.Function;
public class Test {
public static void main(String[] args) {
Function function = new Function<String,Integer>() { //泛型中一个入参 + 一个出参,与里面重写的apply的入参、出参类型对应
@Override
public Integer apply(String str) { //与上方的类型对应
return str.length();
}
};
System.out.println(function.apply("2199"));
}
}
简写为lambda表达式:
Function<String,Integer> function = (str)->{ return str.length(); };
2)断定型接口Predicate:
有一个输入值,和一个返回的bool值(用于判断返回真伪)
Predicate <String> predicate = s -> {return s.isEmpty();}; //lambda表达式来写 System.out.println(predicate.test(""));
3)消费型接口Consumer:
只有入参,没有返回值
Consumer <String> consumer =(str)->{ System.out.println("打印内容:" + str); }; consumer.accept("2199");
4)供给型接口Supplier:
没有入参,只有返回值
Supplier<String> supplier = ()->{return "2199";}; System.out.println(supplier.get());
Stream流式计算
大数据:存储 + 计算
存储 => 集合、MySQL
计算 => 交给流
public class StreamTest { public static void main(String[] args) { User u1 = new User(1,"a",21); User u2 = new User(2,"b",22); User u3 = new User(3,"c",23); User u4 = new User(4,"d",24); User u5 = new User(5,"e",25); User u6 = new User(6,"f",26); User u7 = new User(7,"g",27); //集合用于存储 List<User> list = Arrays.asList(u1,u2,u3,u4,u5,u6,u7); //记住这套路 System.out.println("list = " + list); /** * 题目要求: * 1) id是奇数 * 2) age大于22 * 3) 用户名转换为大写 * 4) 按照用户名倒叙排列 * 5) 限制输出2个用户 * */ //计算用流stream list.stream() .filter(u-> u.getId()%2==1) //.filter里面直接放bool条件 .filter(u-> u.getAge()>22) .map(u->{u.getName().toUpperCase(); return u;}) .sorted((x,y)->{return y.getName().compareTo(x.getName());}) //倒序 .limit(2) .forEach(System.out::println); }//流式计算 + 链式编程 + lambda表达式 + 函数式接口, jdk-8的四大要素都有了 }
jdk-8 时代程序员:lambda表达式、函数式接口、链式编程、Stream流式计算
分支合并 ForkJoin(jdk1.7)
ForkJoin在jdk1.7,并行 执行任务,提高效率,大数据量
大数据MapReduce:把大任务拆分成小任务
ForkJoin特点:工作窃取 => 一个线程干活太快,把别的线程的任务抢过来
双端队列Deque:从两边都可以取出来
代码理解:
public class Test {
@org.junit.Test
public void test1(){
long startTime = System.currentTimeMillis();
Long sum = 0L;
for (Long i = 0L; i<=10_0000_0000; i++){ //10_0000_0000量级
sum += i;
}
long endTime = System.currentTimeMillis();
System.out.println("sum=" + sum + "用时:" + (endTime-startTime) + "ms"); //朴素计算=>5.4秒
}
@org.junit.Test
public void test2() throws ExecutionException, InterruptedException {
long startTime = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTest forkJoinTest = new ForkJoinTest(0L, 10_0000_0000L);
ForkJoinTask<Long> submit = forkJoinPool.submit(forkJoinTest);
Long sum = submit.get();
long endTime = System.currentTimeMillis();
System.out.println("sum=" + sum + "用时:" + (endTime-startTime) + "ms"); //ForkJoin方法(类似递归)=>3.6秒
}
@org.junit.Test
public void test3() throws ExecutionException, InterruptedException {
long startTime = System.currentTimeMillis();
Long sum = LongStream.rangeClosed(0L, 10_0000_0000L).parallel().reduce(0,Long::sum);
long endTime = System.currentTimeMillis();
System.out.println("sum=" + sum + "用时:" + (endTime-startTime) + "ms"); //Stream方法=>0.14秒
}
@org.junit.Test
public void test4(){
long startTime = System.currentTimeMillis();
Long sum = (0L+ 10_0000_0000L) * (10_0000_0000L-0 + 1) /2;
long endTime = System.currentTimeMillis();
System.out.println("sum=" + sum + "用时:" + (endTime-startTime) + "ms"); //公式作弊方法=>0毫秒
}
}
//在10_0000量级:
//朴素计算6ms; ForkJoin用时8ms Stream用时45ms =>和上面的情况完全反过来了
//所以不在大数据量的情况下,Stream不一定就很快
异步回调Async
类似于Ajax(C与S之间) ,不过这里是Java内部的异步调用。
public class Future {
@Test
public void FutureTest1() throws ExecutionException, InterruptedException {
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
try {
TimeUnit.SECONDS.sleep(2);//异步线程耗时,就会返回到主线程继续执行,等到有结果了才会返回来sout
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "runAsync");
});
System.out.println("在主线程ing...");
completableFuture.get(); //获取阻塞执行结果
}
@Test
public void FutureTest2() throws ExecutionException, InterruptedException {
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName());
// int x = 2/0; //导致异常的语句
return 2199;
});
completableFuture.whenComplete((t,u)->{
System.out.println("success."); //正常返回结果
System.out.println("t=>" + t);
System.out.println("u=>" + u);
}).exceptionally((e)->{
System.out.println("exception."); //错误返回结果
System.out.println(e.getMessage());
e.printStackTrace();
return 2333;
});
}
}
volatile
1)保证可见性
2)不保证原子性
3)禁止指令重排
JMM(Java 内存模型)(引出volatile)
指令重排:为了提高性能,编译器和处理器常常会对指令做重排序。
内存屏障:用于保证指令顺序执行。内存屏障分为 LoadLoad、StoreStroe、LoadStore、StoreLoad 四种。(用在volatile前后加)
一、保证可见性
- 每次修改变量后,立刻回写到主内存。
- 在此过程中,会通知其他线程读取新值。
(不然各个线程,存有一个值的 不同副本)
二、不保证原子性
原因:
例如你让一个volatile的integer自增(i++),其实要分成3步:
1)读取volatile变量值到local; 2)增加变量的值;3)把local的值写回,让其它的线程可见。
这3步的jvm指令为:
在内存屏障之前的几步都是不安全的 ==》所以不保证原子性
解决方法:Atomic类
// 2)volatile 不保证原子性,所以就使用Atomic和CAS来保证原子性 public class Test2 { // private volatile static int num =0; private static AtomicInteger num = new AtomicInteger(); //使用原子类的Integer public static void add(){ //num++; //不是一个原子方法 num.getAndIncrement(); //AtomicInteger的+1方法 //用的是底层CAS方法(见下方) //比锁高效非常多倍 } public static void main(String[] args) { for (int i =0;i< 20;i++){ new Thread(()->{ for(int j =0;j<100_0000;j++){ add(); } }).start(); } while(Thread.activeCount()>2){ // main + gc留下,其他都停掉;相当于finally打扫战场 Thread.yield(); } System.out.println(Thread.currentThread().getName() + num); } } //不保证原子性的后果:多线程修改num时,会导致最终计算结果经常不正确(例如下图的例子中,结果就不是正确结果:)
下图中,左边使用AtomicInteger,右边使用普通int(计算结果不正确)
Atomic机理CAS(CompareAndSet,比较并更新)
=> 如果期望的expect值达到了,就set;否则就不更新,并一直循环等(自旋锁)
CAS缺点:
1、循环会耗时
2、一次性只能保证一个共享变量的原子性
3、ABA问题
public class CAS {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2020);
//如果期望的expect值达到了,就set,否则就不更新。CAS是CPU的并发原语
atomicInteger.compareAndSet(2020,2021);
System.out.println(atomicInteger.get()); // true 2021
atomicInteger.compareAndSet(2020,2021);
System.out.println(atomicInteger.get()); // false 2021
}
}
unsafe类:
ABA问题:版本号解决
ABA问题就是:A->B->A,其他线程以为没有变化,但实际上是改变过了的
解决方法就是用时间戳来判断有没有被动过
CAS比较并更新的对象是:
值+stamp 2个内容
三、禁止指令重排
在符合上下指令之间的依赖性的前提下,编译器+执行器,会进行重排。
源代码-->编译器 优化重排-->指令并行 可能会重排-->内存系统 重排 -->执行
volatile写 的前后,加内存屏障,避免指令重排现象。
volatile两种功能:
1)可见性
每个线程都有一块单独内存,存储的共享变量会有不同副本
2)禁止(编译器)指令重排、顺序优化
由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。
这在单线程看起来没什么问题,然而一旦引入多线程,这种乱序就可能导致严重问题。
volatile 和 Synchronized 区别
作用级别:volatile 只用于变量; 变量、方法、类
可见性&原子性:volatile可见性,不保证原子性;synchronized 修改可见性、原子性
阻塞:volatile不阻塞;synchronized 阻塞。
编译器优化:volatile不会优化(内存屏障);synchronized可以优化
深入单例模式(synchronized + volatile)
单例模式:一个类只能构造一个实例对象("构造器私有")
场景:
Windows任务管理器、回收站
项目中,配置文件的类,一般只有一个对象
Spring中的Bean(缓存中取bean很快,减少jvm垃圾回收)(当有请求来的时候会先从缓存(map)里查看有没有,有的话直接使用这个对象,没有的话才实例化一个新的对象)
1)饿汉式单例
==》上来直接new对象,所有类实例化。坏处是:大量浪费不必要的资源(因为很多类 不需要实例化)
2)懒汉式单例
==》按需创建;如果单例已经创建,会返回之前创建的对象。
线程不安全:多个线程可能会并发调用它的getInstance()方法,导致创建多个实例,
因此需要加锁解决线程同步问题( Synchronized 同步锁来修饰 getInstance 方法)
三种线程安全的方法:
方法一:双重锁检测 DCL
==》首先将类加同步锁(syn),但是new语句不是原子操作,所以对了类的实例加volatile锁(可见性)
第一把锁:synchronized锁
第二把锁:volatile锁
方法二:静态内部类
public class Singleton { private static class SingletonHolder { /** * 静态初始化器,由JVM来保证线程安全 */ private static Singleton instance = new Singleton(); } private Singleton(){} public static Singleton getInstance(){ return SingletonHolder.instance; } }
方法三:枚举
==》直接把类名前的class替换成enum就好了,因为枚举无法反射
比较:
使用选择:
一般情况下直接使用饿汉式就好了,
如果明确要求要懒加载(lazy initialization)会倾向于使用静态内部类(比DCL写起来简单),
如果涉及到反序列化创建对象时会试着使用枚举方式来实现单例。