文章目录
- 1.Java中线程的实现方式?
- 2. Java线程中的状态?
- 3. Java中如何停止线程?
- 4. Java中Sleep和wait方法的区别?
- 5.扩展—— P5典型 P6典型 P7典型
- 6.并发编程的三大特性
- 原子性
- 什么时并发编程得原子性
- 保证并发编程得原子性
- 可见性
- 什么时可见性
- 解决可见性的方式
- 有序性
- 什么是有序性
- 7.什么是CAS,有什么优缺点
- 8.Contended注解的作用
1.Java中线程的实现方式?
(1)继承Thread类,重写run方法。
启动线程是调用start()方法,这样会创建一个新的线程,并执行线程任务。
如果直接调用run()方法,这样会让当前线程执行run()方法中的业务逻辑。
代码如下:
public class P1 {
public static void main(String[] args) {
MyJob myJob = new MyJob();
myJob.run();
myJob.start();
for (int j = 0; j < 2; j++) {
System.out.println("start:j="+j);
}
}
}
class MyJob extends Thread{
public void run(){
for (int i = 0; i < 2; i++) {
System.out.println("run:i="+i);
}
}
}
(2) 实现Runnable接口 重写run方法
Thread实现Runnable接口,因此两者追根究底一样。
但由于Java中是单继承,所以相比继承Thread类要好一些。
最常用的方式:
- 匿名内部类:
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("i=" + i);
}
}
});
- lambda方式:
Thread thread = new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println("lambda:"+i);
}
});
}
(3)实现Callable重写call方法,配合FutureTask
Callable一般用于有返回结果的非阻塞的执行方法。
非同步阻塞如果想要返回一个结果,以上两种是没有办法是现实的。
public class P3 {
public static void main(String[] args) throws ExecutionException,InterruptedException {
Mycallables mycallables = new Mycallables();
FutureTask futureTask = new FutureTask(mycallables);
Thread thread = new Thread(futureTask);
thread.start();
//操作
//要结果
Object count = futureTask.get();
System.out.println(count);
}
}
class Mycallables implements Callable{
@Override
public Object call() throws Exception {
int count = 0;
for (int i = 0; i < 10; i++) {
count +=1 ;
}
return count;
}
}
(4) 基于线程池构建线程
追其底层,其实只有一只,就是实现Runnable接口
2. Java线程中的状态?
5种(一般针对操作系统层面):新建状态—start—就绪状态—(CPU调度)—运行状态—(wait()、sleep()、join())—等待状态——结束状态。
Java中给线程准备的6种状态:新建——运行\就绪状态(runnable)——阻塞状态(blocked)——等待(手动唤醒wating)——时间等待(timed_waiting)——结束
NEW:
//New
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
});
System.out.println(t1.getState());
}
// Runnable 就绪/运行状态
public static void main(String[] args) throws InterruptedException {
Thread t2 = new Thread(()->{
while(true){
}
});
t2.start();
Thread.sleep(500);
System.out.println(t2.getState());
}
//Blocked
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
Thread t3 = new Thread(()->{
//t3下拿不到锁资源,导致变成Blocked状态
synchronized (object){
}
});
//main线程拿到obj的锁资源
synchronized (object){
t3.start();
Thread.sleep(500);
System.out.println(t3.getState());
}
}
public static void main(String[] args) throws InterruptedException {
// Waiting
Object object0 = new Object();
Thread t4 = new Thread(()->{
synchronized (object0){
try {
object0.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t4.start();
Thread.sleep(500);
System.out.println(t4.getState());
}
// Timed_Waiting
public static void main(String[] args) throws InterruptedException {
Object object1 = new Object();
Thread t5 = new Thread(()->{
synchronized (object1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t5.start();
Thread.sleep(500);
System.out.println(t5.getState());
}
3. Java中如何停止线程?
(1)stop方法(不推荐,但却是可以做到。过时了)
强制让线程结束的方法有很多,最常用的就是让run方法结束,如论是return结束,还是抛出异常结束,都可以
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
try{
Thread.sleep(5000);
}catch(InterruptedException e){
e.printStackTrace();
}
});
t1.strat();
Thread.sleep(5000);
t1.stop();
System.out.println(t1.getState());
}
(2)使用共享变量(很少会用)
这种方式用的不多,有的线程可能会通过死循环来保证一直运行。
咱们可以通过修改共享变量在破坏死循环,让线程退出循环,结束run方法
static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while(flag){
// 处理任务
}
System.out.println("任务结束");
});
t1.start();
Thread.sleep(500);
flag = false;
(3)interrupt方式
休眠时也可以停掉
/*=============interrupt================*/
//中断标记位 默认形况下 为false
System.out.println(Thread.currentThread().isInterrupted());
// 执行interrupt之后,再次查看打断信息
Thread.currentThread().interrupt();
// interrupt标记位:true
System.out.println(Thread.currentThread().isInterrupted());
// 返回当前线程,并归位为false interrupt标记位:true
System.out.println(Thread.interrupted());
// 已经归位了
System.out.println(Thread.interrupted());
//=======================================
Thread t3 = new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
// 处理业务
}
System.out.println("end");
});
t3.start();
Thread.sleep(500);
t3.interrupt();
4. Java中Sleep和wait方法的区别?
- sleep属于Thread类中的static方法,wait 属于Object类的方法
- sleep属于Timed_waiting,自动被唤醒;wait属于waiting,需要手动唤醒
- sleep方法在持有锁时,执行,不会释放锁资源;wait在执行后,会释放锁资源
- sleep方法在持有锁或者不持有锁时,执行;wait方法必须在持有锁时才可以被执行
wait方法会将持有锁的线程从owner仍到WaitSet集合中,这个操作实在修改ObjectMonitor对象,如果没有持有synchronized锁的话,是无法操作ObjectMonitor对象的。
5.扩展—— P5典型 P6典型 P7典型
P5典型:解释一下什么是乐观锁、悲观锁、自旋锁、读写锁、排他锁、共享锁、统一锁、分段锁、行锁、表锁等。
- 乐观锁:首先来看乐观锁,顾名思义,乐观锁就是持比较乐观态度的锁。就是在操作数据时非常乐观,认为别的线程不会同时修改数据,所以不会上锁,但是在更新的时候会判断在此期间别的线程有没有更新过这个数据。比如数据库提供的类似于write_condition机制,Java API 并发工具包下面的原子变量类就是使用了乐观锁的CAS来实现的。
适用场景:它适用于写少读多的情况,也就是说减少操作冲突,这样可以省去锁竞争的开销,提高系统的吞吐量。
坏事未必会发生,事后补偿 - 悲观锁 :悲观锁就是持悲观态度的锁。就在操作数据时比较悲观,每次去拿数据的时候认为别的线程也会同时修改数据,所以每次在拿数据的时候都会上锁,这样别的线程想拿到这个数据就会阻塞直到它拿到锁。比如行锁、表锁、读锁、写锁,都是在操作之前先上锁,Java API中的synchronized和ReentrantLock等独占锁都是悲观锁思想的实现。
适用场景:它适用于写多读少的情况。因为,如果还使用乐观锁的话,会经常出现操作冲突,这样会导致应用层会不断地Retry,反而会降低系统性能。
坏事一定会发生,预先预防 - 自旋锁:一种常见的乐观锁实现,CS锁
- ABA问题(看似值没有改变,其实已经经过改变如:0-8-0)(解决方法:加版本< version、boolean >)
long sequence ;经历一个操作就要++一次
public class TestAtomicStampedReference {
private static class Order {
long sequence;
long time;
@Override
public String toString() {
return "Order{" +
"sequence=" + sequence +
", time=" + time +
'}';
}
}
// static AtomicStampedReference<Order> orderRef = new AtomicStampedReference<>(new Order(), 0);
static AtomicMarkableReference<Order> orderRef = new AtomicMarkableReference<>(new Order(), false);
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
Order old = orderRef.getReference();
// int stamp = orderRef.getStamp();
Order o = new Order();
o.sequence = old.sequence + 1;
o.time = System.currentTimeMillis();
// orderRef.compareAndSet(old,o,stamp,stamp+1);
orderRef.compareAndSet(old,o,false,true);
}).start();
}
// SleepHelper.sleepSeconds(1);
System.out.println(orderRef.getReference());
}
}
- 保障CAS操作的原则性问题(lock问题)
- 排他锁:只有一个线程能访问代码
- 共享锁:可以允许有多个线程访问代码
- 读写锁:
- 读锁:读的时候,不允许写,但是允许同时读
- 写锁:写的时候,不允许写,也不允许读(排他锁) - 统一锁:大粒度的锁
- 锁定A等待B,锁定B等待A
- A+B统一起来成为大锁 解决死锁问题 - 分段锁:分成一段一段小粒度的锁
- 间隙锁:是一个在 索引记录 之间的间隙上的锁
P6典型
- 对于线程池的理解和运用
- 如何理解线程池的7大参数?
ThreadPoolExecutor tpe = new ThreadPoolExecutor(2,4,60,
TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(4),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
1.corePoolSize: 核心线程数的大小
2.maximunPoolSize:最大线程数大小
3.keepAliveTime:生存时间
4. TimeUnit.SECONDS:生存时间的单位
5.ArrayBlockingQueue(4):任务队列(核心)
6.Executors.defaultThreadFactory():线程池产生的工厂(区分不同的线程抛出的不同异常)
7.RejectedExecutionHandler handler:拒绝策略
估计并发量,确定核心线程数,核心线程永远存在。如餐厅服务员,两个核心员工,消息队列长度为四个,当两个客户占用两个核心员工之后,则从外部调用临时员工(临时员工+核心员工=最大员工数),临时员工服务接下来的客户,之后再服务队列中的客户。(规则由程序来定)。《此中,员工对应线程,客户对应任务》
设置线程的数量能把cpu占满,是跟该线程的IO密集和CPU密集息息相关的。
当线程数全满,又有任务进来的时候,不能够丢掉的任务,可以对其进行持久化。默认有四个策略(jdk)。服务外面可以搭一个mq的集群,扔进mq中,当线程空闲,再拿出来。
有界队列:永远使用有界
无界:如链表队列,尽量不使用
P7典型
- 什么是纤程/协程
- 它和普通的Java线程有什么不同
- 为什么它能提高效率
扩充一下:什么是线程、进程?
一个程序-可执行文件,一般程序在硬盘上,想执行时,从硬盘放入内存里,这是这个程序就是一个进程(为其分配进程空间)。若要运行起来,则需要在进程空间找到线程入口-主线程。
线程是一般称为程序运行的基本单元。
一个CPU一般跑一个线程
线程撕裂者/超线程
6.并发编程的三大特性
原子性
JMM(Java Memory Model)。不同的硬件和不同的操作系统在内存上的操作有一定的差异的。Java为了解决相同代码在不同操作系统上出现的各种问题,用JMM屏蔽掉各种硬件和操作系统带来的差异。
让Java的并发编程可以做到跨平台。
JMM规定所有变量都会存储在主内存中,在操作的时候,需要从主内存中复制一份到线程内存(CPU内存),在线程内部做计算。然后再写回主内存中(不一定)
什么时并发编程得原子性
原子性得定义:一个操作是不可分割得,不可中断得,一个线程再执行时,另一个线程不会影响到他.
保证并发编程得原子性
synchronized:可以在方法上追加synchronized关键字或者采用同步代码块得形式来保证原子性,synchronized可以让避免多线程同时操作临界资源,同一时间点,只会有一个线程正在操作临界资源。
CAS:compare and swap也就是比较和交换,他是一条CPU的并发原语。
他在替换内存的某个位置的值时,首先查看内存中的值与预期值是否一致,如果一致,执行替换操作。这个操作是一个原子性操作。
java中基于Unsafe的类提供了对CAS的操作的方法,JVM会帮助我们将方法实现CAS汇编指令。但是要清楚CAS只是比较和交换,在获取原值的这个操作上,需要你自己实现。
Lock锁
Lock锁在1.5之前,性能相比较synchronized好,1.6之后,synchronized做了优化,性能相差就不大了。如果涉及并发比较多时,推荐ReentrantLock,性能会更好。其底层主要通过lock和unlock来实现。
ThreadLocal
其实,原子性用ThreadLocal很难保证,因为一般原子性是为了避免多线程使用共享变量,从而带来线程不安全。
ThreadLocal保证原子性的方式,是不让多线程去操作临界资源,让每个线程去操作属于自己的数据。
可见性
什么时可见性
可见性问题是基于CPU位置出现的,CPU处理速度非常快,相对于CPU来说,去内存获取数据这个事情太慢了,CPU就提供了L1,L2,L3的三级缓存,每次去主内存拿完数据后,就会储存到CPU的三级缓存,每次去三级缓存拿数据,效率肯定会提升。
这就会带来问题,现在二点CPU都是多核的,每个线程的工作内存(CPU三级缓存)都是独立的,会告知每个线程中做修改时,只改自己额工作内存,没有及时的同步到主内存,导致数据不一致问题。
解决可见性的方式
volatile:修饰成员变量,使用该关键字,相当于告知CPU,对当前属性的操作,不允许使用CPU的缓存,必须去和主存内操作
volatile的内存语义:
- volatile属性被写:当写一个volatile变量,JMM会将当前线程对应的CPU缓存及时刷新到主内存中
- volatile属性被读:当读一个volatile变量,JMM会将对应的CPU缓存中的内存设置无效,必须去主内存中重新读取共享变量。
加了volatile修饰的属性,会在转为汇编之后,追加一个lock的前缀,CPU执行这个指令时,如果带有lock前缀会做两个事情:
- 将当前处理器缓存行的数据写回到主内存
- 这个写回的数据,在其他的CPU内核的缓存中,直接无效
synchronized:加锁的时候将数据,同步到主内存
Lock:与synchronized完全不同,synchronized是基于他的内存语义,在获取和释放锁时,对CPU缓存做一个同步到主内存的操作;而lock锁是基于volatile实现的,Lock锁内部再进行枷锁和释放时,会对一个由volatile修饰和state属性进行加减操作。
final:final和volatile不允许同时修饰一个属性,final修饰的属性不允许再被写了。
有序性
什么是有序性
在Java中,.java文件中的内容会被编译,再执行前需要再次转为CPU可以识别的指令,CPU再执行这些指令时,为了提升执行效率,在不影响最终结果的前提下(满需一些要求),会对指令进行重排。
指令乱序执行的原因,是为了尽可能的发挥CPU的性能。
as-if-serial:
happens-before:
程序次序规则(Program Order Rule):在一个线程内,按照控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。
volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
线程启动规则(Thread Start Rule):Thread对象start()方法先行发生于此线程的每一个动作。
线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法和Thread.isAlive()的返回值等手段检测线程是否已经终止执行。
线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
对象终结规则(Finalizer Rule) :一个对象的初始化完成(构造函数结束)先行发生于它的finalize()方法的开始。
传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
volatile:
7.什么是CAS,有什么优缺点
CAS:compare and swap也就是比较和交换,他是一条CPU的并发原语。
他在替换内存的某个位置的值时,首先查看内存中的值与预期值是否一致,如果一致,执行替换操作。这个操作是一个原子性操作。
java中基于Unsafe的类提供了对CAS的操作的方法,JVM会帮助我们将方法实现CAS汇编指令。但是要清楚CAS只是比较和交换,在获取原值的这个操作上,需要你自己实现。
缺点:CAS只能保证对一个变量的操作是原子性的,无法实现对多行代码实现原子性。
AtomicStampedReference在CAS时,不但会判断原值,还会比较版本信息。
public static void main(String[] args) {
AtomicStampedReference<String>reference = new AtomicStampedReference<>("AAA",1);
String oldValue = reference.getReference();
int oldVersion = reference.getStamp();
boolean b = reference.compareAndSet(oldValue,"B",oldVersion,oldVersion+1);
System.out.println("修改版本1的:"+b);
boolean c = reference.compareAndSet("b","c",1,1+1);
System.out.println("修改版本2的:"+c);
}
自旋时间过长问题:
- 可以指定CAS一共循环多少次,如果超过这个次数,直接失败/或者挂起线程。(自旋锁、自适应自旋锁)
- 可以在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,再返回最后的结果。