Day29
多线程
12. 线程的礼让
Thread.yield();
理解:此方法为静态方法,此方法写在哪个线程中,哪个线程就礼让
注意:所谓的礼让是指当前线程退出CPU资源,并转到就绪状态,接着再抢
需求:创建两个线程A,B,分别各打印1-100的数字,其中B一个线程,每打印一次,就礼让一次,观察实验结果
sleep,yield都是Thread的方法
public class Test01 {
public static void main(String[] args) {
A a = new A();
B b = new B();
a.start();
b.start();
}
}
public class A extends Thread{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("A:" + i);
}
}
}
public class B extends Thread{
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("B:" + i);
//礼让:让当前线程退出CPU资源,当前线程退出后立刻转入抢资源的状态,可能又会抢到CPU资源
Thread.yield();
}
}
}
13. 线程的合并
t.join(); 合并方法
A.join:在API中的解释是:堵塞当前线程B,直到A执行完毕并死掉,再执行=>A先执行完,再执行B;
需求:主线程和子线程各打印200次,从1开始每次增加1,当主线程打印到10之后,让子线程先打印完再打印主线程
public class Test01 {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
for (int i = 1; i <=200; i++) {
System.out.println("主线程:" + i);
if(i == 10){
//让t线程加入到当前线程
t.join();
}
}
}
}
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 1; i <=200; i++) {
System.out.println("子线程:" + i);
}
}
}
14.线程的中断
14.1 线程的中断1
面试题:下列代码的子线程开启后,是否会在3000毫秒就被销毁?
答:不一定,因为3000毫秒后主线程才休眠结束,这时会抢CPU资源
如果立刻抢到,那么子线程就是3000毫秒后销毁
如果没有抢到CPU资源,那么子线程会继续运行,直到主线程抢到CPU资源
public class Test01 {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();//子线程
t.start();
//主线程休眠3秒(Main方法就是主线程)
Thread.sleep(3000);
//过时了
t.stop();//立刻停止(缺点:可能会导致功能缺失)
}
}
public class MyThread extends Thread{
@Override
public void run() {
while(true){ //死循环
System.out.println("111");
System.out.println("222");
System.out.println("333");
System.out.println("444");
}
}
}
14.2 线程的中断2
另外一种写法:
该方法一定会执行到444结束,不像上面一种立马停止,可能在222或者333结束
public class Test01 {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
Thread.sleep(3000);
t.setFlag(false);
}
}
public class MyThread extends Thread{
private boolean flag = true;
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
while(flag){
System.out.println("111");
System.out.println("222");
System.out.println("333");
System.out.println("444");
}
}
}
14.3 线程的中断3
public class Test01 {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
Thread.sleep(3000);
//改变线程状态
t.interrupt();
}
}
public class MyThread extends Thread{
@Override
public void run() {
//获取线程状态(是否消亡)
// System.out.println(Thread.currentThread().isInterrupted());
while(!Thread.currentThread().isInterrupted()){
System.out.println("111");
System.out.println("222");
System.out.println("333");
System.out.println("444");
}
}
}
15.守护线程/后台线程
守护线程 默默守护着前台线程,当所有的前台线程都消亡后,守护线程会自动消亡
注意:垃圾回收器就是守护线程
t.setDaemon(true);
public class Test01 {
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.setDaemon(true);//将当前线程设置为守护线程
t.start();
for (int i = 1; i <= 5; i++) {
System.out.println("主线程:" + i);
Thread.sleep(1000);
}
}
}
public class MyThread extends Thread{
@Override
public void run() {
while(true){
System.out.println("后台线程默默守护着前台线程");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
十六、线程局部变量(实现线程范围内的共享变量)-- ThreadLocal
16.1 共享单个数据
public class Test01 {
public static final ConcurrentHashMap<Thread, Integer> map = new ConcurrentHashMap<>();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
int i = 10;
//存数据
map.put(Thread.currentThread(), i);
A a = new A();
B b = new B();
a.println();//10
b.println();//10
}
},"线程1").start();
new Thread(new Runnable() {
@Override
public void run() {
int i = 20;
//存数据
map.put(Thread.currentThread(), i);
A a = new A();
B b = new B();
a.println();//20
b.println();//20
}
}, "线程2").start();
}
}
public class A {
public void println(){
Thread t = Thread.currentThread();
Integer value = Test01.map.get(t);
System.out.println(t.getName() + "里的A类对象获取了数据:" + value);
}
}
public class B {
public void println(){
Thread t = Thread.currentThread();
Integer value = Test01.map.get(t);
System.out.println(t.getName() + "里的B类对象获取了数据:" + value);
}
}
16.2 共享多个数据-- ThreadLocal
存数据
local.set(data)底层原理:
1.获取当前线程对象
2.通过当前线程对象获取ThreadLocalMap<ThreadLocal,T>
3.map.put(this,t) * 获取数据
local.get()底层原理:
1.获取当前线程对象
2.通过当前线程对象获取ThreadLocalMap<ThreadLocal,T>
3.map.getEntry(this) -> Entry对象
4.entry.getValue()
public class Test01 {
/**
* 知识点:线程局部变量共享的问题 -- 使用ThreadLocal解决该需求
*
* ThreadLocal如何去存储数据的:
* public void set(T value) {
* //获取当前线程对象
Thread t = Thread.currentThread();
//通过当前线程对象获取Map
ThreadLocalMap map = getMap(t);
if (map != null)//如果map不等于null,就直接存储
map.set(this, value);//key-当前ThreadLocal的对象,value-需要共享的值
else//如果map等于null,就创建map,并存储数据
createMap(t, value);
}
public T get() {
//获取当前线程对象
Thread t = Thread.currentThread();
//通过当前线程对象获取Map
ThreadLocalMap map = getMap(t);
if (map != null) {
//获取当前线程中当前ThreadLocal的映射关系对象(Entry<ThreadLocal,E>)
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;//获取到共享的数据
return result;
}
}
return setInitialValue();
}
*/
public static void main(String[] args) {
//线程1
new Thread(new Runnable() {
@Override
public void run() {
//共享的数据
DataPackage dataPackage = DataPackage.getInstance("用良心做教育", 10);
//获取容器,并存储数据
ThreadLocal<DataPackage> local = Container.getLocal();
local.set(dataPackage);
//创建对象
A a = new A();
B b = new B();
//获取共享数据
a.println();
b.println();
}
},"线程1").start();
//线程2
new Thread(new Runnable() {
@Override
public void run() {
//共享的数据
DataPackage dataPackage = DataPackage.getInstance("做真实的自己", 20);
dataPackage = DataPackage.getInstance("拼搏到无能为力", 30);
//获取容器,并存储数据
ThreadLocal<DataPackage> local = Container.getLocal();
local.set(dataPackage);
//创建对象
A a = new A();
B b = new B();
//获取共享数据
a.println();
b.println();
}
},"线程2").start();
}
}
数据包类
//数据包类
public class DataPackage {
private String str;
private int num;
//保证每个线程里只有一个Data包对象
public static DataPackage getInstance(String str, int num){
//获取当前线程共享的DataPackage对象
ThreadLocal<DataPackage> local = Container.getLocal();
DataPackage dataPackage = local.get();
if(dataPackage == null){
dataPackage = new DataPackage(str, num);
local.set(dataPackage);
}else{
dataPackage.setStr(str);
dataPackage.setNum(num);
}
return dataPackage;
}
//有参构造,无参构造,get,set方法省略
}
public class A {
public void println(){
/**
* 获取数据
* local.get()底层原理:
* 1.获取当前线程对象
* 2.通过当前线程对象获取ThreadLocalMap<ThreadLocal,T>
* 3.map.getEntry(this) -> Entry对象
* 4.entry.getValue()
*/
//获取容器
ThreadLocal<DataPackage> local = Container.getLocal();
//通过当前线程获取对应的共享数据
Thread t = Thread.currentThread();
DataPackage data = local.get();
System.out.println(t.getName() + "里的A类对象获取了数据:" + data);
}
public class B {
public void println(){
//获取容器
ThreadLocal<DataPackage> local = Container.getLocal();
//通过当前线程获取对应的共享数据
Thread t = Thread.currentThread();
DataPackage data = local.get();
System.out.println(t.getName() + "里的B类对象获取了数据:" + data);
}
}
//容器类
public class Container {
private static final ThreadLocal<DataPackage> local;
static{
local = new ThreadLocal<>();
}
public static ThreadLocal<DataPackage> getLocal() {
return local;
}
}
什么是 ThreadLocal?
变量值的共享可以使用public static的形式,所有线程都使用同一个变量,如果想实现每一个线程都有自己的共享变量该如何实现呢?JDK中的ThreadLocal类正是为了解决这样的问题。
ThreadLocal为每个线程提供了一个独立的变量副本,避免了多线程间的同步问题。
ThreadLocal类并不是用来解决多线程环境下的共享变量问题,而是用来提供线程内部的共享变量。ThreadLocal 是 Java 提供的一种线程局部变量,它为每个使用该变量的线程都提供了一个独立的副本,从而每个线程都可以独立地改变自己的副本,而不会影响其他线程的副本。
可以将变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立值,不会出现一个线程读取变量时被另一个线程修改的现象。
内部实现机制:
ThreadLocal内部维护了一个ThreadLocalMap。每个线程通过访问自己的ThreadLocalMap来访问ThreadLocal变量的副本。ThreadLocalMap的键 是ThreadLocal对象本身,值是该ThreadLocal对象所持有的副本数据。(key-当前ThreadLocal的对象,value-需要共享的值)
说说ThreadLocal底层实现?
ThreadLocal底层是通过ThreadLocalMap实现,key -> ThreadLocal; value-> 需要存储的值。
注意事项
内存泄漏问题:由于 ThreadLocalMap 使用的是 ThreadLocal 的弱引用,但值是强引用,如果线程池长时间不释放线程,可能会导致内存泄漏。因此,使用 ThreadLocal 时应在不再需要时调用 remove() 方法清理变量。适用场景:ThreadLocal 适用于每个线程需要独立的实例或数据的场景,不适用于需要线程间共享数据的场景。
性能问题:对于频繁创建和销毁线程的场景,ThreadLocal 的创建和销毁开销可能较大,因此更适合于线程池等长生命周期的线程管理场景。
17.线程的生命周期
1、新建状态
i. 在程序中创建了一个线程对象后,新的线程对象便处于新建状态,此时,它已经有了内存空间和其它资源,但还处于不可运行状态。新建一个线程对象可采用线程构造方法来实现。
ii. 例如:Thread thread=new Thread();
2、 就绪状态
i. 新建线程对象后,调用该线程的start()方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待CPU调用,这表明它已经具备了运行条件。
3、运行状态
i. 当就绪状态的线程被调用并获得CPU资源时,线程就进入了运行状态。此时,自动调用该线程对象的run()方法。run()方法定义了该线程的操作和功能。
4、 阻塞状态
i. 一个正在执行的线程在某些特殊情况下,如join()、sleep()、wait()等方法,线程都将进入阻塞状态。阻塞时,线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。
5、死亡状态
i. 线程调用stop()方法时或run()方法执行结束后,线程即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。
总结
1.线程的礼让 – yield
2.线程的合并 – join
3.线程的中断
4.守护线程
5.线程局部变量共享 – 重要
简答题
1.什么是多线程,多线程的优劣?
多线程:在一个程序中可以同时运行多个不同的线程来执行不同的任务。
多线程的好处:
可以提高 CPU 的利用率。
多线程的劣势:
线程也是程序,所以线要消耗内存,线程越多占用内存也越多;
多线程需要协调和管理,所以需要 CPU 时间跟踪线程;
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
2.创建线程有四种方式:
继承 Thread 类;
实现 Runnable 接口;
实现 Callable 接口;
使用 ThreadPoolExecutors 工具类创建线程池
3.守护线程和用户线程有什么区别呢?
用户线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
守护线程:运行在后台,为其他前台线程服务。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作。main 函数所在的线程就是一个用户线程,main 函数启动的同时在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。
4.什么是线程局部变量?
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java 提供 ThreadLocal 类来支持线程局部变量,是一种实现线程安全的方式。但是使用线程局部变量的时候要特别小心,任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
6.Thread类中的yield方法有什么作用?
让当前线程退出CPU资源,并转到就绪状态,接着再抢。
它是一个静态方法而且只保证当前线程放弃CPU而不能保证其它线程一定能占用CPU,执行yield()的线程有可能在进入到就绪状态后马上又被执行。
7.sleep()和wait() 有什么区别?
类的不同:sleep() 是 Thread 类的静态方法,wait() 是 Object 的方法
释放锁:sleep() 不释放锁:当前线程将睡眠 n 毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进行就绪状态,等待 CPU 的到来;
wait() 释放锁:必须与 synchronized 关键字一起使用,线程进入阻塞状态,当 notify 或者 notifyall 被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,释放互斥锁。
用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。
8.如何停止一个正在运行的线程
1、使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2、使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend一样都是过期作废的方法。
3、使用interrupt方法中断线程。
wait(); notify();notifyAll()都是Object类中的方法
sleep,yield 是Thread类中的方法
10.为什么 wait, notify 和 notifyAll 这些方法不在 thread类里面?
一个很明显的原因是 JAVA 提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。由于 wait,notify 和 notifyAll 都是锁级别的操作,所以把他们定义在 Object 类中因为锁属于对象。
11.为什么 wait 和 notify 方法要在同步块中调用?
Java API 强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException 异常。还有一个原因是为了避免 wait 和 notify之间产生竞态条件。
12.同步方法和同步块,哪个是更好的选择?
同步块是更好的选择,因为它不会锁住整个对象。同步方法会锁住整个对象,哪怕这个类中有多个不相关的同步块,这会导致他们停止执行并需要等待获得这个对象上的锁。同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,这样从侧面来说也可以避免死锁。
13.Java 中你怎样唤醒一个阻塞的线程?
利用 Object 类的 wait()和 notify()方法实现线程阻塞。
首先,wait、notify 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对象阻塞的线程,但它需要重新获取改对象的锁,直到获取成功才能往下执行;
其次,wait、notify 方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。
14.怎么检测一个线程是否拥有锁?
在 java.lang.Thread 中有一个方法叫 holdsLock(),它返回 true 如果当且仅当当前线程拥有某个具体对象的锁。
15.为什么 Thread 类的 sleep()和 yield ()方法是静态的?
Thread 类的 sleep()和 yield()方法是在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。
总结
1.线程的礼让 – yield
2.线程的合并 – join
3.线程的中断
4.守护线程
5.线程局部变量共享 – 重要
6.线程生命周期 — 重要