java多线程与线程同步
一个对象是否安全取决于是否被多个线程访问,最终依旧能获得预期的效果。
框架通过在框架线程中调用应用程序代码将并发性引入到程序中。在代码中将不可避免地访问应用程序状态,因此所有访问这些状态的代码路径都必须是线程安全的。
创建多线程的方法
有两种方法来创建一个新的执行线程。一是声明一个类是一类
Thread
。这类应重写类Thread
的run
方法。子类的一个实例可以被分配和启动。
(面试)使用那种创建线程更好?
使用Runnable 更好
1.从代码架构角度
2.新建线程的损耗
3.java不支持双继承
- 解耦的角度用Runnable更好
- Thread会单独创建一个线程–节约资源
- 使用Thread 不能多继承 --大大限制了扩展性
- 框架可以进行 线程池优化
(面试)创建线程有几种方式?
通常来说我们分为两类:Oracle也是这么说的
准确来说,创建一个线程只有一种方法那就是使用Thread类实现线程的执行单元只有两种。
- 方法一:实现Runnable接口的run方法,并把Runnable实例传给Thread类
- 方法二:重写Thread的run()方法(继承Thread类)
错误启动线程的观点:
- 1.线程池是一种线程创建方式
package xyz.amewin.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author Amewin
* @author 匿名内部类方法
* @date 2020/8/17 19:22
*/
public class TestThead2 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i <1000 ; i++) {
executorService.submit(new Tack(){});
}
}
}
class Tack implements Runnable{
@Override
public void run() {
System.out.println("--------");
try{
Thread.sleep(500);
}catch (Exception e){
}
System.out.println(Thread.currentThread().getName());
}
}
-
2.定时器也是新的线程创建方式
-
3.匿名内部类
-
4.lambda表达式
/**
* 描述: lambda表达式创建线程
*/
public class Lambda {
public static void main(String[] args) {
new Thread(() -> System.out.println(Thread.currentThread().getName())).start();
}
}
启动线程 Run()与Start()的区别?
启动主线程必须使用start()间接调用run方法
-
Start()启动新线程 --请求jvm启动 --由线程调度器统一启动
-
准备工作
- 启动新线程检查线程状态
- 加入线程组
- 调用start0()
-
不能重复调用Start() --java.lang.IllegalThreadStateException
-
线程状态不符合规定
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 可运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。 - 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
-
既然start()方法会调用run()方法,为什么我们选择调用start()方法,而不是直接调用run()方法呢?
run只是一个普通的方法 start才是真正意义上的创建一个新的线程
如何停止线程?
-
interruot
使用interrupt来通知,而不是强制
通常线程会在什么情况下停止普通情况?
使用try catch 进行异常处理
阻塞代码sleep被调用是会 抛出一个异常sleep interrupted sleep会检测并会清除
try {
while (num <= 3000 &&!Thread.currentThread().isInterrupted()) {
if (num % 100 == 0) {
System.out.println(num + "是100的倍数");
}
num++;
}
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
2.如果线程在每次迭代后都阻塞
正确的停止线程的方法(Interrupt)<通知中断>
-
whlie 内try/catch的问题
java设计时当线程出现异常会把
java.lang.InterruptedException: sleep interrupted
清除导致无法使用
Thread.currentThread().isInterrupted()
在whlie循环内无法停止线程 -
实际开发中两种最佳实践方法
- 最佳实践:catch了InterruptedExcetion之后的优先选择:在方法签名中抛出异常 那么在run()就会强制
- 佳实践2:在catch子语句中调Thread.currentThread().interrupt()来恢复设置中断状态,以便于在后续的执行中,依然能够检查到刚才发生了中断* 回到刚才RightWayStopThreadInProd补上中断,让它跳出
-
响应中断的方法总结
正确停止的方法:
package xyz.amewin.stopthreads;
/**
* 描述: 最佳实践:catch了InterruptedExcetion之后的优先选择:在方法签名中抛出异常 那么在run()就会强制try/catch
*/
public class RightWayStopThreadInProd implements Runnable {
@Override
public void run() {
//判断线程是否终止 !Thread.currentThread().isInterrupted()
while (true && !Thread.currentThread().isInterrupted()) {
System.out.println("go");
try {
throwInMethod();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
//保存日志、停止程序
System.out.println("保存日志");
e.printStackTrace();
}
}
}
private void throwInMethod() throws InterruptedException {
Thread.sleep(2000);
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new RightWayStopThreadInProd());
thread.start();
Thread.sleep(1000);
//向jvm提出中断线程
thread.interrupt();
}
}
Thread常用方法
threadOne.start();//启动线程
//设置中断标志 -- 中断这个线程。
threadOne.interrupt();
//测试当前线程是否已被中断。--测试当前线程是否已被中断。
threadOne.interrupted();
/**
*注意 interrupted 该方法为静态方法
* --从属于当前类,无法响应别的线程--
**/
//获取中断标志
threadOne.isInterrupted();
//获取中断标志并重置
threadOne.interrupted();
//获取中断标志并重直 ---测试当前线程是否已被中断。
Thread.interrupted();
//获取中断标志
threadOne.isInterrupted();
stop
会导致运行的线程突然停止,没办法完成一个基本的单位的操作,或造成脏数据
suspend(线程挂起)
如果别的线程不及时唤醒线程,容易造成锁未解放,容易造成死锁。
-
resume(线程挂起) -
volatile 设置boolean标记位 --JMM方式理解
演示用volatile的局限part2 陷入阻塞时,volatile是无法线程的此例中,生产者的生产速度很快,消费者消费速度慢,长时间阻塞队列满了以后,生产者会阻塞,等待消费者进一步消费
介绍
- 不保证原子性
- 保证不发生重排序
- 可见性 (线程之间同步,某一个线程进行修改时,则会则会使其他使用该变量的线程的属性失效)
使用场景
线程之间的某一属性赋值
不适合使用场景
i++ ;流程之间的判读
注意
double long 数据类型不需要保证volatile 其底层实现就已经,完成了原子性的实现
Sychronized的作用
同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个
对象对多个线程可见,则对该对象变量的所有读取或写入都是通过同步方法
完成的。
能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并
发安全的效果。
Synchronized的地位
-
Synchronized是Java的关键字,被Java语言原生支持
-
是最基本的互斥同步手段
-
是并发编程中的元老级角色,是并发编程的必学内容
Sychronized的用法
-
1.对象锁
- 类锁、方法锁和同步代码块锁(自己指定对象锁)
使用方法:
- 使用this关键字
- 新建object = new object();
Object obj = new Object();
@Override
public void run() {
//对象锁
//synchronized (obj){
synchronized (object){
for (int j = 0; j < 10000; j++) {
i++;
}
}
}
- 方法锁
//方法锁
private synchronized void sumNum() {
//打印线程名称
System.out.println(Thread.currentThread().getName());
for (int j = 0; j < 10000; j++) {
i++;
}
System.out.println("线程结束");
}
- 2.类锁
- 指sychronized修饰的类中的静态方法锁定的class
线程出现异常解决方案
- 优先选择传递中断 – 放置在主函数中<最上层处理>
- 保存日志、处理中断
- 不想活无法传递:恢复中断
- 不应屏蔽中断
- 底层函数不因处理由顶层函数处理
synchronized面试题
1.两个线程同时访问一个对象的同步方法?
会同步锁,访问同一个锁
2.两个线程访问的是两个对象的同步方法?
互不干扰 --是两个锁
3.两个线程访问的是synchronized的静态方法?
静态类方法从属于类 --所以该方法同步
4.同时访问同步方法与非同步方法?
非同步方法不受影响
5.访问同一个对象的不同的普通同步方法
会出现锁之间的串行情况
6.同时访问静态synchronized和非静态synchronized方法?
依旧可以同时运行,因为这是两把不同锁。
7.方法抛异常会释放锁?
jvm会自己释放锁
总结
- 一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对
应第1、5种情况) ; - 每个实例都对应有自己的一把锁,不同实例之间互不影响;例外:
锁对象是*.class以及synchronized修饰的是static方法的时候, 所有
对象共用同一把类锁(对应第2、3、4、6种情况) ; - 无论是方法正常执行完毕或者方法抛出异常, 都会释放锁(对应第7
种情况)
Synchromized 性质
- 可重入
什么是可重入?:
指的是同一线程的外层函数获得锁之后, 内层函
数可以直接再次获取该同一线程的继承和接口有权访问被锁定的对象
在java内部,同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入
好处:避免死锁,提升封装性
粒性:线程而非调用(用3种情况来说明和pthread的区别)
2. 不可中断
一旦这个锁已经被别人获得了,如果我还想获得,我只能选择等待或者
阻塞,直到别的线程释放这个锁。如果别人永远不释放锁,那么我只能永
远地等下去。
原理
1.加锁和释放锁的原理: 现象、时机、深入JVM看字节码
2.可重入原理:加锁次数计数器
3.保证可见性的原理:内存模型
可重入原理:加锁次数计数器
◆JVM负责跟踪对象被加锁的次数
◆线程第一次给对象加锁的时候,计数变为1。每当这个相同的线程在此对象.上再次获得锁时,计数会递增
◆每当任务离开时 ,计数递减,当计数为0的时候,锁被完全释放
反编译说明
被synchronized修饰的代码块,会被JVM锁定,只有被解锁之后才能操作。
synchronized的缺陷
1.效率低
效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中
断一个正在试图获得锁的线程
2.不够灵活(读写锁更灵活) :
加锁和释放的时机单一, 每个锁仅
有单一的条件(某个对象),可能是不够的
3.无法知道是否成功获取到锁
synchronized常见面试题
1.synchronized 使用注意点?
锁对象不能为空,作用域不能过大,避免死锁
2.如何选择lock和synchronized关键字?
- 最好都不使用
- 使用juc并发类进行处理
- 适用synchronized 优先选用
- 减少代码编写
3.多线程访问同步线程的具体情况
总结
一句话个绍synchronized
JVM会自动通过使用monitor来加锁和解锁,保证了同时只有一
个线程可以执行指定代码,从而保证了线程安全,同时具有可重
入和不可中断的性质。
Java异常体系
如何正确的停止发生异常的子线程?
- 方案一:(不推荐)手动在每个run 方法中进行try eatch
- 方案二:(推荐) 利用Uncaught ExceptionHandler
面试题
-
如何正确停止线程?
1.原理:用interrupt来请求、好处
2.想停止线程,要请求方、被停止方、子方法被调用方相互配合
3.最后再说错误的方法: stop/suspend已废弃,volatile的boolean
无法处理长时间阻塞的情况 -
如何处理不可中断的阻塞
使用可以响应的中断
thread.wait() 等待线程
thread.notiy()唤醒当前线程
thread.notiyAll()唤醒所有线程
package xyz.amewin.Consumer;
import java.util.Date;
import java.util.LinkedList;
/**
* 描述: 用wait/notify来实现生产者消费者模式
*/
public class ProducerConsumerModelTeacher {
public static void main(String[] args) {
EventStorage eventStorage = new EventStorage();
Producer producer = new Producer(eventStorage);
Consumer consumer = new Consumer(eventStorage);
new Thread(producer).start();
new Thread(consumer).start();
}
}
//生产者
class Producer implements Runnable {
private EventStorage storage;
public Producer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.put();
}
}
}
//消费者
class Consumer implements Runnable {
private EventStorage storage;
public Consumer(EventStorage storage) {
this.storage = storage;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
storage.take();
}
}
}
/**
* 实现阻塞队列
*/
class EventStorage {
private int maxSize;
private LinkedList<Date> storage;
public EventStorage() {
maxSize = 10;
storage = new LinkedList<>();
}
//放入方法<生产者>
public synchronized void put() {
/**
* 当生产者的数量大于最大值时,停止生产
*/
while (storage.size() == maxSize) {
try {
//停止生产
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
storage.add(new Date());
System.out.println("仓库里有了" + storage.size() + "个产品。");
notify();
}
//进行消费<消费者>
public synchronized void take() {
while (storage.size() == 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("拿到了" + storage.poll() + ",现在仓库还剩下" + storage.size());
notify();
}
}
线程状态
//查看线程状态
Thread min = Thread.currentThread();
min.getStackTrace();
面试题
用程序实现两个线程交替打印0~100的奇偶数?
-
第一种:创建两个线程实现两个线程接口类
- 单独进行判断
- 可以使用 wait()、notify()进行优化
-
第二种:创建一个接口类线程类(使用wait()、notify()、进行相互等待、唤醒,使程序变得有序)
public class PintNum100Two implements Runnable {
// 1.创建锁
/**
* 在一把锁内的线程,只能被某一条线程使用
* 别的线程将会阻塞
*
*/
private static final Object lock = new Object();
private static int count = 0;
@Override
public void run() {
while (count <= 100) {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
//换醒别线程
lock.notify();
//如果到达100 就不执行了 结束线程
if(count<=100){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) {
Thread thread1 = new Thread(new PintNum100Two());
Thread thread2 = new Thread(new PintNum100Two());
thread1.start();
thread2.start();
}
}
第二种
public class PintNum100 {
private static int count;
private static final Object lock = new Object();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (count <= 100) {
synchronized (lock) {
if ((count & 1) == 0) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
try {
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}, "偶数").start();
new Thread(new Runnable() {
@Override
public void run() {
while (count <= 100) {
synchronized (lock) {
if ((count & 1) == 1) {
System.out.println(Thread.currentThread().getName() + ":" + count++);
try {
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}, "奇数").start();
;
}
}
手写生产者消费者设计模式?
为什么wait()需要在同步代码块内使用,而sleep()不需要?
因为不在同步代码块中使用将出现巨大的线程错误,执行wait无法保证线程不被唤醒,在不加锁的情况下可能出现线程不安全。
sleep针对的方向是当前线程,所以不需要额外的代码保护
为什么线程通信的方法wait()与notify()和notifyAll()被定义在Object类里?而sleep定义在Thread类里?
锁是保存在对象中,而不是线程中。wait、notify 、notifyAll 针对的是锁级别的操作
wait方法是属于Object对象的,那调用Thread.wait会怎么样?
不要使用Thread 作为锁
如何选择用notify还是nofityAll ?
nofityAll 唤醒全部线程
notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?
会继续等待
用suspend()和resume()来阻塞线程可以吗?为什么?
该方法太过时,不适合用于线程阻塞
Thread和Object类中的重要方法详解
-
sleep 特点
-
为不需要暂用cpu资源的线程降低cpu的占用
-
不释放锁
- synchronized 和 lock
- 和wait 不同
sleep优雅的写法
TimeUnit.DAYS.sleep(1); TimeUnit.MINUTES.sleep(10); TimeUnit.SECONDS.sleep(10);
总结:
sleep方法可以让线程进入Waiting状态,并且不占用CPU资源,但是不释放锁,直到规定时间后再执行,休眠期间如果被中断,会抛出异常并清除中断状态。
-
面试题
wait/notify. sleep异同 (方法属于哪个对象?线程状态怎么切换? )
- 相同点
◆阻塞
◆响应中断 - 不同点
◆同步方法中
◆释放锁 wait<释放锁> sleep<不释放锁>
◆指定时间
◆所属类
join 方法
官方
等待该线程是否死亡作用
因为新的线程加入了我们,所以我们要等他执行完再出发
用法
主线程等待子线程完成,再执行工作
详解
主线程在等待子线程完成工作的同时,主线程将会中断,是谁唤醒它的呢?
是jvm底层实现唤醒他的
注意点:
◆ CountDownL .atch或CyclicBarrier类
面试题
1.在join期间,线程处于哪种线程状态?
waiting
2.什么时候我们需要设置守护线程?
不需要,只有一个用户线程时(不管业务是否执行完,jvm都可能进行回收)
我们应该如何应用线程优先级来帮助程序运行?有哪些禁忌?
使用UncaughtExceptionHandler 进行全局异常处理
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* 描述: 自己的MyUncaughtExceptionHanlder
*/
public class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
private String name;
public MyUncaughtExceptionHandler(String name) {
this.name = name;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
Logger logger = Logger.getAnonymousLogger();
logger.log(Level.WARNING, "线程异常,终止啦" + t.getName());
System.out.println(name + "捕获了异常" + t.getName() + "异常");
}
}
使用方法
public class ExceptionTest implements Runnable {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandlerTest("异常捕捉器"));
try {
new Thread(new ExceptionTest(), "t1").start();
Thread.sleep(100);
new Thread(new ExceptionTest(), "t2").start();
Thread.sleep(100);
new Thread(new ExceptionTest(), "t3").start();
Thread.sleep(100);
new Thread(new ExceptionTest(), "t4").start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
throw new RuntimeException();
}
}
面试题
1.如何全局处理异常?为什么要全局处理?不处理行不行?
Thread.UncaughtExceptionHandler 进行全局处理
可以是引用Logger 进行打印日志
这样存在程序内部出现异常抛出前台的风险,导致被白帽或黑帽拿到信息
2.run方法是否可以抛出异常?如果抛出异常,线程的状态会怎么样?
不可以,终止运行( TERMINATED)
3.线程中如何处理某个末处理异常?
使用全局处理
线程安全
什么情况下会出现线程安全问题,怎么避免?
- 运行结果错误: a+ +多线程”下出现消失的请求现象
- 活跃性问题:死锁、活锁、饥饿
- 对象发布和初始化的时候的安全问题
什么是逸出?
private 了一个对象,不小心创建了getxxx方法,返回了这个对象,结果别的线程获得该对象,进行了修改,使得别方法操作了该对象获取不到值导致逸出。
1.方法返回一个private对象 ( private的本意是不让外部访问)
2.还未完成初始化 (构造函数没完全执行完毕)就把对象提供给外界,比如:
3.在构造函数中未初始化完毕就this赋值
package xyz.amewin.background;
/**
* 描述: 初始化未完毕,就this赋值
*/
public class MultiThreadsError4 {
static Point point;
public static void main(String[] args) throws InterruptedException {
new PointMaker().start();
// Thread.sleep(10);
Thread.sleep(105);
if (point != null) {
System.out.println(point);
}
}
}
class Point {
private final int x, y;
public Point(int x, int y) throws InterruptedException {
this.x = x;
MultiThreadsError4.point = this;
Thread.sleep(100);
this.y = y;
}
@Override
public String toString() {
return x + "," + y;
}
}
class PointMaker extends Thread {
@Override
public void run() {
try {
new Point(1, 1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//1,1
◆隐式逸出----- 注册监听事件
观察者模式逸出的原因:
观察者模式在观察某一个类对象时,已经进行了初始化导致,如果观察者创建类对象的时机,后于观察类会导致观察者观察失败。—
◆在构造函数中运行线程<新建线程>
数据连接池、工厂模式、会默认创建连接池
如何解决逸出?
1.创建一个副本
2.使用工厂模式
各种需要考虑线程安全的情况 (重点)
- 访问共享的变量或资源,会有并发的风险
- 依赖于业务顺序的操作,可能存在线程安全
- 不同数据之间的捆绑关系
◆访问共享的变量或资源,会有并发风险,比如对象的属性、静态变量、
共享缓存、数据库等
所有依赖时序的操作,即使每一步操作都是线程安全的,还是存在并发
问题: read-modify-write、check-then-act
◆不同的数据之间存在捆绑关系的时候
◆我们使用其他类的时候,如果对方没有声明自己是线程安全的
多线程带来的问题
-
调度:上下文切换
-
什么是上下文?–线程调度器进行调度 导致线程暂停
- 保存现场
- 缓存开销: <缓存失效>
导致密集上下文切换的原因:
竞争锁比较激烈 、 IO
b.缓存开销 --CPU重新缓存
-
-
协同:内存同步
- 为了数据正确性,同步手段往往会禁用编译器优化,是cpu内的缓存失效