JUC并发编程(面试)

1:同步与异步                                                 16:park()与unpark()
2:start()和run()                                               17:park(),unpark()&wait(),notify()
3:sleep() 与 yield()                                         18:死锁
4:线程优先级                                                 19:Lock接口
5:join()
6:join(毫秒数)
7:interrupt() 方法
8:提前结束线程
9:守护线程
10:临界区与竞态条件(如何解决)
11:synchronized
12:线程8锁
13:线程安全性分析
14:wait()和notify(),notifyAll()
15:sleep()与wait()方法对比


同步与异步:

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步


start()和run()

区别:

  • 调用start()开启一个新的线程,调用run()方法就是普通方法的调用,不会创建新线程。
  • 调用start()方法,会执行run()方法中的代码。
  • start()方法不能多次调用,多次调用会出现IllegalThreadsStateException异常。


sleep() 与 yield()

sleep():让线程进入睡眠

  • 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)。

  • 其它线程可以使用 interrupt() 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException。

  • 睡眠结束后的线程未必会立刻得到执行,会先进入就绪状态等待cpu分配时间片。

  • 建议用 TimeUnit(jdk1.5之后) 的 sleep 代替 Thread 的 sleep 来获得更好的可读性。

    //都是睡眠1秒
    TimeUnit.SECONDS.sleep(1);
    Thread.sleep(1000);
    

yield():让运行状态线程进入就绪

  • 调用 Thread.yield() 会让当前线程放弃当前时间片,从 Running(运行状态) 进入 Runnable 就绪状态,然后调度执行其它线程
  • 具体的实现依赖于操作系统的任务调度器

二者区别:

  • sleep()会释放当前线程占有的时间片,直到睡眠时间结束或被interrupt() 打断后,cpu才会重新为其分配时间片等待cpu重新调用。
  • yield():同样会让出时间片,等待cpu重新为其分配时间片,cpu重新调度,只不过没有睡眠时间。


线程优先级

线程优先级后会提示任务调度器去优先调度该线程,但仅仅只是提示,最终的任务调度情况还得由调度器决定,比如cpu比较繁忙时,优先级高的线程会获得更多的时间片。而cpu空闲时,优先级甚至会被忽略进而导致不起作用

设置优先级:

 t1.setPriority(n);
 //n[ 1 , 10 ]
 //1:最小优先级
 //5:默认优先级
 //10:最大优先级


join():让当前线程等待另一个线程执行结束后再继续执行当前线程

案例:

    static int r = 0;
    public static void main(String[] args) throws InterruptedException {
        test1();
    }
    private static void test1() throws InterruptedException {
        log.debug("开始");
        Thread t1 = new Thread(() -> {
            log.debug("开始");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("结束");
            r = 10;
        });
        t1.start();
        t1.join();
        log.debug("结果为:{}", r);
        log.debug("结束");
    }
当前案例执行结果为 **r=0** 而不是 **r=10**

分析:

因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
而主线程一开始就要打印 r 的结果,所以只能打印出 r=0。

解决方案:
想要让最终结果为r=10,就需要让主线程等待t1线程执行完毕后,主线程再去打印最终结果(异步变为同步)。即调用join() 方法。



join(毫秒数):当前线程等待另一个线程

解释: 当前线程等待另一个线程执行,与join() 这个无参数的方法不同,join()方法会让当前线程等待另一个线程结束。而join(500) 表示当前线程最多等待另一个线程500毫秒,如果在5秒没另一个线程没有执行完毕,将不再等待,继续执行当前线程。

static int r1 = 0;
    public static void main(String[] args) throws InterruptedException {
        test3();
    }

    public static void test3() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            r1 = 10;
        });
        long start = System.currentTimeMillis();
        t1.start();
        // 线程执行结束会导致 join 结束
        t1.join(500);
        long end = System.currentTimeMillis();
        log.debug("r1: {} r2: {} cost: {}", r1, end - start);
    }
当前案例执行结果为 **r=0** 而不是 **r=10**


interrupt() 方法

  • 打断sleep,wait,join 的线程 打断标记为false,并抛出异常

      
       Thread t1 = new Thread(() -> {
           try               
           TimeUnit.SECONDS.sleep(1);//睡眠1000毫秒
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           }, "t1");
       t1.start();
       Thread.sleep(500);//让主线程睡眠500毫秒,防止后面代码先被执行
       t1.interrupt();
       log.debug(" 打断状态: {}", t1.isInterrupted());//打印打断标记
          
    
    结果:打断标记为false
    
  • 打断正常运行的线程 打断标记为true,不会抛出异常

    private static void f() throws InterruptedException {
            Thread t2 = new Thread(() -> {
                while (true) {
                    Thread current = Thread.currentThread();//返回线程执行状态信息
                    boolean interrupted = current.isInterrupted();//返回线程打断标记
                    if (interrupted) {
                        log.debug(" 打断状态: {}", interrupted);
                        break;
                    }
                }
            }, "t2");
            t2.start();
            Thread.sleep(500);
            t2.interrupt();
        }
    
    结果:打断标记为true
    


提前结束线程

点击观看相关黑马程序员视频

  • 方式1:stop()方法,由于种种原因已被废弃
  • 方式2:调用System.exit(int)方法,但是这个方法会停止整个进程,小题大做。
  • 方式3:调用interrupt()方法,通过两阶段终止模式进行终止。

两阶段终止模式:
在这里插入图片描述
代码实现:

package cn.itcast.test;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.Test6")
public class test6 {
    public static void main(String[] args) throws InterruptedException {
        Moniter m = new Moniter();
        m.startOK();

        Thread.sleep(5000);
        m.stopOK();
    }
}

//监控类
@Slf4j(topic = "c.Moniter")
class Moniter {
    private Thread moniter;

    //启动监控线程
    public void startOK() {
        moniter = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();//获取当前线程信息
                if (current.isInterrupted()) { //判断是否被打断
                    System.out.println("料理后事");//如果被打断
                    break;
                }
                try {//如果没有被打断,进行监控操作
                    Thread.sleep(2000);// 如果这里被打断,打断标记会变为false,同时执行catch代码块中代码
                                            // (至于为什么会走catch代码块,请到文章开头,点击->interrupt() 方法)
                    log.debug("执行监控记录");//  如果这里被打断,打断标记会变为true
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    current.interrupt();//重新设置打断标记
                }
            }
        });

        moniter.start();
    }

    //停止当前线程
    public void stopOK() {
        moniter.interrupt();
    }
}

结果:
在这里插入图片描述
补充细节:

  • isInterrupted():返回打断标记。
  • interrupt():返回打断标记,如果打断标记为true,将打断标记改为false。


守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

log.debug("开始运行...");
Thread t1 = new Thread(() -> {
 log.debug("开始运行...");
 sleep(2);
 log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");

注意:

  • 垃圾回收器线程就是一种守护线程。
  • Tomcat 中的 AcceptorPoller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求。


临界区与竞态条件

  • 临界区: 一段代码块内如果存在多个线程对共享资源的读写操作,称这段代码块为临界区

    让线程t1加a 5000次,线程t2减a 5000(下面的代码结果不为0)
    public class test6 {
    
    private static int a = 0;
    
    public static void main(String[] args) throws InterruptedException {
    
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++)
                a++;
        }, "t1");
    
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++)
                a--;
        }, "t1");
    
        t1.start();
        t2.start();
    
        t1.join();
        t2.join();
        System.out.println(a);
    }
    

}
```

  • 竞态条件: 多个线程在临界区内执行,此时出现线程的上下文切换,称之为发生了竞态条件

synchronized 解决方案

  • 阻塞式的解决方案:synchronized,Lock

(俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换)
语法

synchronized(对象)
{
 临界区
}

正确代码:

public class test6 {

    private static int a = 0;
    private static Object lockOK = new Object();

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++)
                synchronized (lockOK) {
                    a++;
                }

        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++)
                synchronized (lockOK) {
                    a--;
                }
        }, "t1");

        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(a);
        }
    }
}

附加信息

  • 非阻塞式的解决方案:原子变量


synchronized 跳转链接

  • 加在成员方法上
public class Test6 {
//synchronized 锁的是调用当前方法的对象
    public synchronized void test() {
    }
}
等价于
public class Test6 {
    public void test() {
        synchronized (this) {
        }
    }
}
  • 加在静态方法上:
public class Test6 {
//synchronized 锁的是当前类对象:Test6.class
    public synchronized static void test() {

    }
}
等价于
public class Test6 {
    public static void test() {
        synchronized (Test6.class) {
        }
    }
}


线程安全性分析:

  • 成员变量和静态变量

    • 如果它们没有共享,则线程安全
    • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
      • 如果只有读操作,则线程安全
      • 如果有读写操作,则这段代码是临界区,需要考虑线程安全
  • 局部变量

    • 局部变量是线程安全的
    public static void test1() {
    	int i = 10;
        i++;
    }
    

    (每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享)

    • 但局部变量引用的对象则未必 点击观看尚硅谷视频
      • 如果该对象没有逃离方法的作用范围,它是线程安全的
      • 如果该对象逃离方法的作用范围,需要考虑线程安全


wait()和notify(),notifyAll()
在这里插入图片描述

  • obj.wait() 让当前线程释放锁进入 object 监视器的线程到 waitSet(等待区) 等待
  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
  • obj.wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify
  • 它们都是线程之间进行协作的手段,都属于 Object 对象的方法必须获得此对象的锁,才能调用这几个方法
		new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        }).start();
        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait(); // 让线程在obj上一直等待下去
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码....");
            }
        }).start();
        // 主线程两秒后执行
        sleep(2);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
            obj.notify(); // 唤醒obj上一个线程
            // obj.notifyAll(); // 唤醒obj上所有等待线程
        }
    }


sleep()与wait()方法对比

  • sleep() 是Thread中的方法,wait() 是Object中的方法。
  • sleep() 不需要强制和 synchronized 配合使用,但 wait() 需要和 synchronized 一起用。
  • sleep() 在睡眠的同时,不会释放对象锁,但 wait() 在等待的时候会释放对象锁。


park()与unpark()

LockSupport.park(); // 暂停当前线程

LockSupport.unpark(暂停线程对象); // 恢复某个线程的运行

代码案例:

案例1:线park在unpark

Thread t1 = new Thread(() -> {
 log.debug("start...");
 sleep(1);
 log.debug("park...");
 LockSupport.park();
 log.debug("resume...");
},"t1");
t1.start();
sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);		

结果:
在这里插入图片描述
案例2:先 unpark 再 park

Thread t1 = new Thread(() -> {
 log.debug("start...");
 sleep(2);
 log.debug("park...");
 LockSupport.park();
 log.debug("resume...");
}, "t1");
t1.start();
sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);

结果:
在这里插入图片描述
实现线程有序执行

点击观看黑马程序员视频



park(),unpark()&wait(),notify()

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必.
  • park & unpark 是以线程为单位,可以精确的【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】。
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify。


死锁

跳转链接

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁

例:
t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁 。
t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁。

代码:(多次运行后总有一次发生死锁情况)

public class Test {
    private static final X x = new X();
    private static final Y y = new Y();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            synchronized (x) {
                System.out.println("获得X锁");
                synchronized (y) {
                    System.out.println("获得Y锁");
                }
            }
        },"线程1").start();

        new Thread(() -> {
            synchronized (y) {
                System.out.println("获得X锁");
                synchronized (x) {
                    System.out.println("获得Y锁");
                }
            }
        },"线程2").start();
    }
}

class X {
}

class Y {
}

定位死锁

  • 第一步: 终端输入jps查看所有java进程
    在这里插入图片描述
  • 第二步: 终端输入jstack 进程号查看当前java进程的具体信息
    在这里插入图片描述


Lock接口

Lock与synchronized的区别:

  • Lock不是Java语言中内置的,synchronized是Java一样中的关键字。
  • 采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放锁。
  • 采用Lock则必须要用户去手动释放锁,如果没有手动释放锁,会有死锁的可能。
  • 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
  • 在性能上来说,如果竞争资源不激烈,两者的性能差不多的,而当资源竞争非常激烈时(有大量线程同时竞争时),此时Lock的性能要远优于synchronized,所以Lock提高多个线程进行读操作的效率。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值