Day30
1.作业
1.1 线程类解决
1.计算任务,一个包含了2万个整数的数组,分拆了多个线程来进行并行计算,最后汇总出计算的结果。
注意:使用线程类的方式解决该需求
public class Test01 {public static void main(String[] args) throws InterruptedException {//创建数组int[] arr = new int[20000];//初始化数组数据 -- {1,2,3,....,20000}for (int i = 0; i < arr.length; i++) {arr[i] = i+1;}//创建线程MyThread t1 = new MyThread(0, 5000, arr);MyThread t2 = new MyThread(5000, 10000, arr);MyThread t3 = new MyThread(10000, 15000, arr);MyThread t4 = new MyThread(15000, 20000, arr);//启动线程t1.start();t2.start();t3.start();t4.start();//问题出现原因:四个子线程还没有运行完毕,就被主线程抢到CPU资源了//解决思路:让四个子线程执行完毕后,主线程才能执行 --- 主线程阻塞!!!//解决方案一:
// Thread.sleep(6);//解决方案二:
// while(t1.isFlag() || t2.isFlag() || t3.isFlag() || t4.isFlag()){}//解决方案三:t1.join();t2.join();t3.join();t4.join();//合并子线程的运行结果int result = t1.getSum() + t2.getSum() + t3.getSum() + t4.getSum();System.out.println(result);}
}
public class MyThread extends Thread{private int startIndex;private int endIndex;private int[] arr;public MyThread(int startIndex, int endIndex, int[] arr) {this.startIndex = startIndex;this.endIndex = endIndex;this.arr = arr;}private int sum;private boolean flag = true;@Overridepublic void run() {for (int i = startIndex; i < endIndex; i++) {sum += arr[i];}flag = false;}public int getSum() {return sum;}public boolean isFlag() {return flag;}
}
1.2 任务类解决
注意:使用任务类的方式解决该需求
public class Test01 {public static void main(String[] args) throws InterruptedException {//创建数组int[] arr = new int[20000];//初始化数组数据 -- {1,2,3,....,20000}for (int i = 0; i < arr.length; i++) {arr[i] = i+1;}//创建任务Task task1 = new Task(0, 5000, arr);Task task2 = new Task(5000, 10000, arr);Task task3 = new Task(10000, 15000, arr);Task task4 = new Task(15000, 20000, arr);//创建线程Thread t1 = new Thread(task1);Thread t2 = new Thread(task2);Thread t3 = new Thread(task3);Thread t4 = new Thread(task4);//启动线程t1.start();t2.start();t3.start();t4.start();//问题出现原因:四个子线程还没有运行完毕,就被主线程抢到CPU资源了//解决思路:让四个子线程执行完毕后,主线程才能执行 --- 主线程阻塞!!!//解决方案一:
// Thread.sleep(6);//解决方案二:
// while(task1.isFlag() || task2.isFlag() || task3.isFlag() || task4.isFlag()){}//解决方案三:t1.join();t2.join();t3.join();t4.join();//合并任务的运行结果int result = task1.getSum() + task2.getSum() + task3.getSum() + task4.getSum();System.out.println(result);}
}
public class Task implements Runnable{private int startIndex;private int endIndex;private int[] arr;public Task(int startIndex, int endIndex, int[] arr) {this.startIndex = startIndex;this.endIndex = endIndex;this.arr = arr;}private int sum;private boolean flag = true;@Overridepublic void run() {for (int i = startIndex; i < endIndex; i++) {sum += arr[i];}flag = false;}public int getSum() {return sum;}public boolean isFlag() {return flag;}}
1.线程安全
1.1 线程类解决
1.1.1 同步代码块
线程安全 – 加锁
注意:要想多个线程互斥住,就必须使用同一把锁(对象)!!! 重要
synchronized
同步代码块:
synchronized(锁对象){//自动上锁
…想要互斥的代码…
}//自动解锁
Lock
需求:铁道部发布了一个售票任务,要求销售1000张票,要求有3个窗口来进行销售,请编写多线程程序来模拟这个效果
窗口001正在销售第1张票
窗口001正在销售第2张票
窗口002正在销售第3张票
。。。
窗口002正在销售第1000张票
窗口002票已售完
窗口001票已售完
窗口003票已售完
注意:使用线程类的方式解决该需求
问题一:三个窗口各卖1000张票,一共卖了3000张
出现原因:allTicket和curTicket是run方法的局部变量,三个线程抢到CPU资源后,都会调用run方法,run方法被调用了3次,所以卖了3000张票
解决方案:将allTicket和curTicket设置为静态变量,让三个线程共享
问题二:有些票没有卖,有些票卖了重票
出现原因:当前线程抢到CPU资源后做了票的自增,但是还没来得及输出,时间片到了就退出CPU资源,然后其他线程抢到CPU资源了
解决方案:当前线程抢到CPU资源后,票的自增和输出执行完毕才能切换到其他线程运行 – 加锁
问题三:多卖了票
出现原因:curTicket到了临界点(999),三个线程都可以进判断,然后上锁
解决方案:在锁中再次判断
注意:锁对象使用static非常不好
public class Test01 {public static void main(String[] args) {MyThread t1 = new MyThread("001");MyThread t2 = new MyThread("002");MyThread t3 = new MyThread("003");t1.start();t2.start();t3.start();}
}
public class MyThread extends Thread{private static int allTicket = 1000;private static int curTicket = 0;private static Object obj = new Object();public MyThread(String name) {super(name);}@Overridepublic void run() {while(curTicket < allTicket){//都是锁对象//synchronized (String.class) {//synchronized ("abc") {synchronized (obj) {if(curTicket < allTicket){curTicket++;System.out.println("窗口" + Thread.currentThread().getName() + "正在销售第" + curTicket + "张票");}if(curTicket >= allTicket){System.out.println("窗口" + Thread.currentThread().getName() + "票已经售完");}}}}
}
同步代码块解决数据安全问题【应用】
安全问题出现的条件
- 是多线程环境
- 有共享数据
- 有多条语句操作共享数据
如何解决多线程安全问题呢?
- 基本思想:让程序没有安全问题的环境
怎么实现呢?
- 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
- Java提供了同步代码块的方式来解决
同步代码块格式:
synchronized(任意对象) {
多条语句操作共享数据的代码
}
synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁
同步的好处和弊端
- 好处:解决了多线程的数据安全问题
- 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率
1.1.2 同步方法
synchronized
同步代码块:
synchronized(锁对象){//自动上锁
…想要互斥的代码…
}//自动解锁
同步方法
同步方法 – 成员同步方法:
注意:锁对象 -> this
public synchronized void method(){//自动上锁
…想要互斥的代码…
}//自动解锁
同步方法 – 静态同步方法:
注意:锁对象 -> 类.class
public static synchronized void method(){//自动上锁
…想要互斥的代码…
}//自动解锁
public class Test01 {public static void main(String[] args) {MyThread t1 = new MyThread("001");MyThread t2 = new MyThread("002");MyThread t3 = new MyThread("003");t1.start();t2.start();t3.start();}
}
public class MyThread extends Thread{private static int allTicket = 1000;private static int curTicket = 0;public MyThread(String name) {super(name);}@Overridepublic void run() {while(curTicket < allTicket){method();}}//一定是要加static,因为不加锁对象是this,锁不住//锁对象:MyThread.classpublic static synchronized void method(){if(curTicket < allTicket){curTicket++;System.out.println("窗口" + Thread.currentThread().getName() + "正在销售第" + curTicket + "张票");}if(curTicket >= allTicket){System.out.println("窗口" + Thread.currentThread().getName() + "票已经售完");}}}
同步方法解决数据安全问题【应用】
同步方法的格式
同步方法:就是把synchronized关键字加到方法上
修饰符 synchronized 返回值类型 方法名(方法参数) {
方法体;
}
同步方法的锁对象是什么呢?
this
静态同步方法
同步静态方法:就是把synchronized关键字加到静态方法上
修饰符 static synchronized 返回值类型 方法名(方法参数) {
方法体;
}
1.1.3 Lock锁
Lock
//锁对象
Lock lock = new ReentrantLock();
//要加 try…catch…
lock.lock();//手动上锁 写在 synchronized的位置,代替
…想要互斥的代码…
lock.unlock();//手动解锁
public class Test01 {public static void main(String[] args) {MyThread t1 = new MyThread("001");MyThread t2 = new MyThread("002");MyThread t3 = new MyThread("003");t1.start();t2.start();t3.start();}
}
public class MyThread extends Thread{private static int allTicket = 1000;private static int curTicket = 0;//必须是静态的,因为有三个对象,让线程所有对象都共享private static Lock lock = new ReentrantLock();public MyThread(String name) {super(name);}@Overridepublic void run() {while(curTicket < allTicket){lock.lock();//手动上锁try {if(curTicket < allTicket){curTicket++;System.out.println("窗口" + Thread.currentThread().getName() + "正在销售第" + curTicket + "张票");}if(curTicket >= allTicket){System.out.println("窗口" + Thread.currentThread().getName() + "票已经售完");}} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();//手动解锁}}}}
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
ReentrantLock构造方法
ReentrantLock():创建一个ReentrantLock的实例
加锁解锁方法
lock():获得锁
unlock():释放锁
1.2 任务类解决
1.2.1 同步代码块
需求:铁道部发布了一个售票任务,要求销售1000张票,要求有3个窗口来进行销售,请编写多线程程序来模拟这个效果
窗口001正在销售第1张票
窗口001正在销售第2张票
窗口002正在销售第3张票
。。。
窗口002正在销售第1000张票
窗口002票已售完
窗口001票已售完
窗口003票已售完
注意:使用任务类的方式解决该需求
(与上面的不一样)
问题一:三个窗口各卖1000张票,一共卖了3000张
出现原因:三个线程抢到CPU资源后都会调用run方法,curTicket和allTicket都是run方法里的局部变量,所以会调用3次
解决思路:curTicket和allTicket设置为成员属性,三个线程共用同一个任务
问题二:有些票没有卖,有些票卖了重票
出现原因:当前线程抢到CPU资源后做了票的自增,但是还没来得及输出,时间片到了就退出CPU资源,然后其他线程抢到CPU资源了
解决方案:当前线程抢到CPU资源后,票的自增和输出执行完毕才能切换到其他线程运行 – 加锁
问题三:多卖了票
出现原因:curTicket到了临界点(999),三个线程都可以进判断,然后上锁
解决方案:在锁中再次判断
public class Test01 {public static void main(String[] args) {Task task = new Task();Thread t1 = new Thread(task, "001");Thread t2 = new Thread(task, "002");Thread t3 = new Thread(task, "003");t1.start();t2.start();t3.start();}
}
public class Task implements Runnable{private int allTicket = 1000;private int curTicket = 0;@Overridepublic void run() {while(curTicket < allTicket){synchronized (this) {//使用this,不用objif(curTicket < allTicket){curTicket++;System.out.println("窗口" + Thread.currentThread().getName() + "正在销售第" + curTicket + "张票");}}}System.out.println("窗口" + Thread.currentThread().getName() + "票已售完");}}
1.2.2 同步方法
public class Test01 {public static void main(String[] args) {Task task = new Task();Thread t1 = new Thread(task, "001");Thread t2 = new Thread(task, "002");Thread t3 = new Thread(task, "003");t1.start();t2.start();t3.start();}
}
public class Task implements Runnable{private int allTicket = 1000;private int curTicket = 0;@Overridepublic void run() {while(curTicket < allTicket){method();}System.out.println("窗口" + Thread.currentThread().getName() + "票已售完");}//用成员同步方法public synchronized void method(){if(curTicket < allTicket){curTicket++;System.out.println("窗口" + Thread.currentThread().getName() + "正在销售第" + curTicket + "张票");}}}
1.2.3 Lock锁
Lock锁要加try…catch
public class Test01 {public static void main(String[] args) {Task task = new Task();Thread t1 = new Thread(task, "001");Thread t2 = new Thread(task, "002");Thread t3 = new Thread(task, "003");t1.start();t2.start();t3.start();}
}
public class Task implements Runnable{private int allTicket = 1000;private int curTicket = 0;private Lock lock = new ReentrantLock();@Overridepublic void run() {while(curTicket < allTicket){lock.lock();try {if(curTicket < allTicket){curTicket++;System.out.println("窗口" + Thread.currentThread().getName() + "正在销售第" + curTicket + "张票");}} catch (Exception e) {e.printStackTrace();} finally {lock.unlock();}}System.out.println("窗口" + Thread.currentThread().getName() + "票已售完");}}
注意:一般解决需求,使用任务类,更灵活
简答题
关于 Java多线程,在面试的时候,问的比较多的就是①悲观锁和乐观锁( 具体可以看我的这篇文章:面试必备之乐 观锁与悲观锁)、②synchronized和lock区别以及volatile和synchronized的区别**,③可重入锁与非可重入锁的区 别、④多线程是解决什么问题的、⑤线程池解决什么问题、⑥线程池的原理、**⑦线程池使用时的注意事项、⑧AQS原理、⑨ReentranLock源码,设计原理,整体过程等等问题。
面试官在多线程这一部分很可能会问你有没有在项目中实际使用多线程的经历。所以,如果你在你的项目中有实际使用Java多线程的经历 的话,会为你加分不少哦!
1.什么是线程安全?
线程安全就是说多线程访问同一代码,不会产生不确定的结果:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量 的值也和预期的是一样的,就是线程安全的。
在多线程环境中,当各线程不共享数据的时候,即都是私有(private)成员,那么一定是线程安全的,但这种情况并不多见,在多数情况下需要共享数据,这时就需要进行适当的同步控制了。
线程安全一般都涉及到synchronized, 就是一段代码同时只能有一个线程来操作,不然中间过程可能会产生不可预制的结果。
2…说一说自己对于synchronized关键字的了解
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态 转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优 化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
3…说说自己是怎么使用synchronized关键字,在项目中用到了吗?
synchronized关键字最主要的三种使用方式:
修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如 果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的 静态 synchronized 方法,是许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁 是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因 为JVM中,字符串常量池具有缓存功能!
下面我已一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。
面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!”
双重校验锁实现对象单例(线程安全)
public class Singleton { private volatile static Singleton uniqueInstance; private Singleton() { } public static Singleton getUniqueInstance() { //先判断对象是否已经实例过,没有实例化过才进入加锁代码 if (uniqueInstance == null) { //类对象加锁 synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
1.为 uniqueInstance 分配内存空间
2.初始化 uniqueInstance
3.将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3, 此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
4.讲一下 synchronized*关键字的底层原理
synchronized 关键字底层原理属于 JVM 层面。
① synchronized 同步语句块的情况
public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("synchronized 代码块"); } } }
在cmd控制台通过 javap 命令查看 SynchronizedDemo 类的相关字节码信息:
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter **指令指向同步代码块的开始位置,**monitorexit 指令则指明同步代码块的结束位置。
当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等 待,直到锁被另外一个线程释放为止。
Java中synchronized和ReentrantLock有什么不同?
相似点:
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一 个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的.
区别:
这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需 要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。
由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized, ReentrantLock类提供了一些高级功能,主要有以下3项:
1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。
2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象。
谈谈synchronized和ReenTrantLock的区别
① 两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
③ ReenTrantLock 比 synchronized 增加了一些高级功能
相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;
③可实现选择性通知(锁可以绑定多个条件)
ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的ReentrantLock(boolean fair) 构造方法来制定是否是公平的。
synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视 器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由JVM选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果 执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的 signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。
④ 性能已不是选择标准
总结
1.评讲作业
2.买票的案例 – 线程安全 (重要)
1.完全掌握卖票案例
2.理解对象内存结构 - https://blog.csdn.net/weixin_44606481/article/details/1348024193.线程安全 — 买票案例
synchronized代码块
synchronized方法(成员同步方法、静态同步方法)
Lock锁
注意:
1.加锁的方式
2.锁对象(多个线程去操作同一把锁才能互斥住)