线程常见问题

1、死锁

概念:由于两个或多个线程互相持有对方需要的资源,导致这些线程处于等待状态,无法继续执行。

死锁原因:当前线程拥有其他线程需要的资源、当前线程等待其他线程已拥有的资源、都不放弃自己拥有的资源。

如何避免死锁:

  • 固定加锁顺序:很有效,但是需要事先知道所有可能会用到的锁,但总有时候是无法预知的。
  • 加锁时限:尝试获取锁时加一个超时时间,超时后该线程就放弃对该锁的请求,回退并释放所有已获得的锁,然后等待一段时间再重试,适合少量线程。
  • 死锁检测:更好的死锁预防机制,主要针对不可能实现按序加锁和锁超时的场景。

jps命令(java自带)查看当前运行的java进程。jstack -l 进程id:查看进程的死锁。

2、线程中锁的种类

  • 公平锁:多线程按照申请锁的顺序来获取锁,先来后到。比如同时有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程会获得锁。
  • 非公平锁:无法保证按照申请顺序获得锁,有可能造成优先级反转或饥饿现象。但是吞吐量比公平锁大。

Synchronized、ReentrantLock都是非公平锁,ReentrantLock可以在构造时传入参数true,就是公平锁。

  • 可重入锁:同一个线程在外层方法获得锁,进入内层方法会自动获得锁。如果不能重入,线程已经拥有了这个锁,又再申请这个锁,永远也达不到锁,可重入锁一定程度可以避免死锁。

Synchronized、Lock都是可重入锁。

  • 自旋锁:线程在没有取得锁的时候不被挂起,而是去执行空循环(即自旋),若干个空循环后,该线程如果可以获得锁,则继续执行,否则才会被挂起。优点:减少线程上下文切换的消耗,缺点:循环会消耗CPU。适用:锁竞争不是很激烈,锁占用时间很短的并发线程。

自旋锁开启与自旋次数:由虚拟机自动开启并自动调整次数。

  • 读写锁:区分读锁和写锁,允许多个读,只允许一个写操作,提高了执行效率。

读写锁接口:ReadWriteLock,实现类:ReentrantReadWriteLock。lock.readLock()获取读锁,lock.writeLock()获取写锁。

  • 可中断锁:Synchronized锁不可中断,lock锁可以中断等锁的行为。
  • 独享锁/共享锁:大部分都是独享锁,ReadWriteLock的读锁就是共享锁。
  • 分段锁:不是一种锁的设计,ConcurrentHashMap通过分段锁实现高效的并发操作。

3、Volatile 关键字

  • 只能修饰变量,变量的值可能随时会被别的线程修改,而且会强制将修改的值立即写入内存,所以每个线程得到的都是volatile最新的值。
  • 可见性(每个线程可以立刻读取到其他线程修改后的值)、不具备原子性。

不具备原子性是与synchronized、lock最大的区别。某一线程从主内存获取到共享变量的值,当其修改完变量值重新写入主内存时,并没有去判断主内存的值是否发生改变

  • synchronized 关键字可以保证变量原子性和可见性。
  • 最适合一个线程写、多个线程读的场景。

4、HashMap在多线程环境下的使用

线程不安全。

两个或者多个 rehash 可能会导致 map 中同一个 buket 下链表数据形成一个环,这时再调用get方法就会出现死循环。

最好使用HashTable(效率极低)或ConcurrentHashMap。

5、守护线程(Daemon)

  • 概念:用于为系统中的其他对象和线程提供服务,在没有用户线程时自动离开。将一个用户线程设置为守护线程的方法:在线程对象创建之前调用线程对象的setDaemon(true)方法。
  • 典型的守护线程:垃圾回收线程。当程序中没有任何的Thread,就不会再产生垃圾,虚拟机自动离开。
  • 守护线程中产生的线程也是守护线程,用户线程一样。
  • 使用场景:为其他线程提供服务支持。执行一些后台任务,程序退出时,线程自动关闭。

5、ThreadLocal

  • 概念:线程局部变量。只在线程的生命周期内有效,采用空间换时间。
  • 原理:ThreadLocal中有一个静态内部类ThreadLocalMap,用来存变量,key为当前ThreadLocal对象,value为存入的值。
  • 方法:
ThreadLocal threadLocal = new ThreadLocal();
//底层是调用:Thread t = Thread.currentThread();  
//ThreadLocalMap map = t.threadLocals;
threadLocal.set("222");
//底层是调用:Thread t = Thread.currentThread();  
//ThreadLocalMap map = t.threadLocals;
//map.set(threadLocal, "222");
Object obj = threadLocal.get();
//底层是调用:Thread t = Thread.currentThread();  
//ThreadLocalMap map = t.threadLocals;
//ThreadLocalMap.Entry e = map.getEntry(threadLocal);  
//Object obj = e.value;

注意:ThreadLocalMap 是使用 ThreadLocal 的弱引用作为key,GC 时 key 被回收,无法再访问 key 为 null 的 value,如果 value 是较大对象而且使用线程池,此时线程不退出,容易导致OOM。所以线程逻辑代码结束时,必须调用ThreadLocal.remove() 。

6、多线程上下文切换

  • 概念:CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

即使单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片很短,一般几十毫秒,让我们感觉同时执行。

  • 多线程不一定快:并发不超过百万时,串行执行比多线程快。因为线程有创建和上下文切换的开销。
  • 减少上下文切换,提高多线程运行效率:
    • 无锁并发编程:多线程竞争锁,会引起上下文切换。可以按照数据ID的Hash值分段,不同线程处理不同段的数据。
    • CAS算法:JAVA的Atomic包使用CAS算法来更新数据,不需要加锁。
    • 使用最少线程:避免创建不需要的线程。
    • 协程:在单线程里实现多任务调度,并维持多个任务的切换。

7、可以运行时kill掉一个线程吗?可以,但是不建议

  • 舍弃stop方法:强制终止线程,就像突然关闭计算机电源,线程的资源来不及释放,容易产生数据不完整。
  • 使用共享变量
public class ThreadFlag extends Thread {
    private volatile boolean exit = false;
    @Override
    public void run() {
        while (!exit);
    }
    public static void main(String[] args) throws InterruptedException {
        ThreadFlag threadFlag = new ThreadFlag();
        threadFlag.start();
        sleep(3000);
        threadFlag.exit = true;
        threadFlag.join();
        System.out.println("线程退出");
    }
}
  • 使用thread.interrupt(),线程阻塞时退出阻塞状态,抛出InterruptedException异常。

8、线程串行执行

  • thread.join():需要等待上一个线程(也就是调用此方法的thread)执行完以后才能执行当前线程。
public class ThreadByOrder {

    public static void main(String[] args){
        thread1.start();
        thread2.start();
        thread3.start();
    }

    static Thread thread1 = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("执行:" + Thread.currentThread().getName());
        }
    });

    static Thread thread2 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                thread1.join();//需要等待thread1执行结束以后才开始执行当前thread2线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("执行:" + Thread.currentThread().getName());
        }
    });

    static Thread thread3 = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                thread2.join();//需要等待thread2执行结束以后才开始执行当前thread3线程
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("执行:" + Thread.currentThread().getName());
        }
    });

}
  • CountDownLatch
public class ThreadTest2 {
    //实现线程t1、t2、t3依次执行
    public static void main(String[] args){
        CountDownLatch c0 = new CountDownLatch(0); //t1计数器为0
        CountDownLatch c1 = new CountDownLatch(1); //t2计数器为1
        CountDownLatch c2 = new CountDownLatch(1); //t3计数器为1

        Thread t1 = new Thread(new Worker(c0, c1));//c0为0,t1可以执行。t1的计数器减1
        Thread t2 = new Thread(new Worker(c1, c2));//t1的计数器为0时,t2才能执行。t2的计数器c2减1
        Thread t3 = new Thread(new Worker(c2, c2));//t2的计数器c2为0时,t3才能执行

        t1.start();
        t2.start();
        t3.start();
    }

    static class Worker implements Runnable {
        CountDownLatch c1;
        CountDownLatch c2;
        public Worker(CountDownLatch c1, CountDownLatch c2) {
            super();
            this.c1 = c1;
            this.c2 = c2;
        }
        @Override
        public void run() {
            try {
                c1.await();
                System.out.println("Thread start" + Thread.currentThread().getName());
                c2.countDown();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值