面试深入之java多线程
使用多线程的方式
1.继承Thread类
缺点:java语言不支持多继承
Thread类底层也是实现的Rannable,执行run方法的调用顺序与其执行顺序无关
2.实现Runnable接口
在run方法前添加 synchronized关键字,可以使多个线程在执行 run 方法时,以排队的方式进行处理。
多线程共享变量可能引发的问题
实例与变量的线程安全问题
1.在使用synchronized关键字修饰方法时,在多线程情况下会出现指令重排问题
在某些 JVM 中, i– 的操作要分成如下 3 步:
1)取得原有ⅰ值。
2)计算 i-1。
3)对i进行赋值。
在这3个步骤中,如果有多个线程同时访问,那么一定会出现非线程安全问题。
2.一个书中的案例:
用println打印全局变量的$i–$,在多线程环境下会引发线程安全问题,因为:
虽然$println()$方法在内部是同步的,但$i–$的操作却是在进入$println()$之前发生的,所以有发生非线程安全问题的概率,
常用方法
1.currentTread0方法
可以返回该方法在被哪个线程调用,书中案例为:构造方法被main线程调用,run方法被Thread线程调用
isAlive()方法
可以判断线程当前是否在活动状态
停止线程
1.interrupet()停止线程
书中案例:在for循环中停止的线程,for循环下面的语句还可以正常输出;要解决这一问题,可以将代码块放入try中,并在for循环内抛出异常
2.在sleep状态下停止线程
如果在sleep状态下停止线程,则会进入catch语句,并清除停止的状态值
如果先停止再sleep,则会进入catch
3.用stop()方法停止线程
会造成数据不一致的后果,所以已经作废
暂停线程
suspend与resume方法
缺点:
1.独占:在使用suspend与resume方法时,如果使用不当,极易造成公共的同步对象的独占,使得其他线程无法访问公共同步对象。
2.不同步:会造成因线程暂停导致数据不同步的情况
线程的优先级
可以使用setPriority()
方法设置线程的优先级,优先级为$1-10$
优先级具有继承的特性:A线程调用的B线程,B线程会继承A线程的优先级
线程先执行不代表先执行完,线程的优先级具有随机性
守护线程
线程分为2种,一种为用户线程,一种为守护线程
main线程和GC线程是经典的两个守护线程
对象和变量的并发访问
“非线程安全”其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,
产生的后果就是“脏读”,也就是取到的数据其实是被更改过的。而“线程安全”就是以获得的实例变量的值是经过同步处理的,不会出现脏读的现象。
方法内的变量是线程安全的
synchronized
1.锁的是谁?
关键字synchronized 取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁 Lock;
也就是说,一个线程调用某个对象的方法时,这个对象就被锁住了,如果有另一个线程调用该对象的其他方法,就只能等待持有锁的线程把锁释放
但如果多个线程访问多个对象,则 JVM 会创建多个锁。
2.synchronized的性质
1.是可重入锁
可重人锁的概念是:自己可以再次获取自己的内部锁。
比如有1条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可重人的话,就会造成死锁。
2.是悲观锁
3.出现异常自动释放
4.同步不可继承
父类中的方法用synchronized修饰,子类在继承这个方法时,不会同时继承锁
3.弊端及其解决方案(同步代码块)
如果一个对象长时间的持有锁,其他对象只能等待,就会等待很久
解决方案:使用同步代码块
方法内部的同步代码块,形如:
synchronized (this) {
//you code
}
而不是在方法上加synchronized,这样就做到了:在方法中:不在代码块中的语句异步执行,代码块中的语句同步执行
当有多个同步代码块时,当线程访问一个同步代码块,其他代码块将被阻塞;即多个同步代码块之间也是同步(按顺序执行的)
synchronized与synchronized (this)都是锁的对象
4.synchronized与synchronized (this)异同
书上的大段原话
多个线程调用同一个对象中的不同名称的 synchronized 同步方法或 synchronized(this)同步代码块时,调用的效果就是按顺序执行,也就是同步的,阻塞的。这说明synchronized 同步方法或 synchronized(this)同步代码块分别有两种作用。
1.synchronized同步方法:
- 对其他synchronized 同步方法或 synchronized(this) 同步代码块调用呈阻塞状态。
- 同一时间只有一个线程可以执行 synchronized 同步方法中的代码。
2.synchronized(this)同步代码块
- 对其他synchronized 同步方法或 synchronized(this) 同步代码块调用呈阻塞状态。
- 同一时间只有一个线程可以执行 synchronized(this)同步代码块中的代码。
其实Java还支持对“任意对象”作为“对象监视器”来实现同步的功能。这个“任意对象”大多数是实例变量及方法的参数,使用格式为synchronized(非this对象).
5.同步代码块synchronized(任意对象)做对象监视器
1)当多个线程同时执行 synchronized(x)(} 同步代码块时呈同步效果。
2)当其他线程执行ⅹ对象中synchronized 同步方法时呈同步效果。
3)当其他线程执行ⅹ对象方法里面的synchronized(this)代码块时也呈现同步效果。
但需要注意:如果其他线程调用不加 synchronized 关键字的方法时,还是异步调用。
6.synchronized应用在静态方法前锁Class
和将 synchronized 关键字加到非static 方法上使用的效果是一样的。其实还是有本质上的不同的,synchronized 关键字加到static静态方法上是给 Class 类上锁,而synchronized 关键字加到非static 静态方法上是给对象上锁。
7.synchronized与String连用时的问题
synchronized(String)使用时会由于jvm中有string常量池缓存功能,导致多个线程持有相同的锁,所以同步代码块不与String连用
如果非要实现字符串的操作可以用new String()解决,原因如下:
Object a = new String("A");
Object b = new String("B");
System.out.println(a.hashCode()==b.hashCode());
String c = "B";
String d = "B";
System.out.println(c.hashCode()==d.hashCode());
//输出结果为:
//false
//true
原因猜想:用new关键字,定义出两个不同的字符串对象,直接写两个相同的字符串,在常量池中是同一个对象
volatile
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。
与vlolatile实现相关一些名词解释
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
Java内存模型的基础
在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步
线程之间的通信机制有两种:共享内存和消息传递。
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型。
1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句
的执行顺序。
2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level
Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应
机器指令的执行顺序。
3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上
去可能是在乱序执行。
2和3属于处理器重排序
JMM的编译器重排序规则会禁止特定类型的编译器重排
序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要
求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为
Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。