多线程,线程同步问题

进程和线程区别?

进程是资源分配的最小单位,线程是CPU调度的最小单位。进程间互不干扰,相互独立。线程可以用来共享数据。进程是运行中的程序,线程是进程的内部的一个执行序列。 进程是资源分配的单元,线程是执行单元。进程间切换代价大,线程间切换代价小多个线程共享进程的资源。进程包括至少一个线程,每个进程对应一个JVM实例,多个线程共享JVM里的堆。Java采用单线程编程模型,程序会自动创建主线程。

进程与线程的选择取决以下几点:

  1. 需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的。
  2. 线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应并行操作时使用线程;
  3. 需要更稳定安全时,适合选择进程;需要速度时,选择线程更好。

并发和并行

并发:指应用能够交替执行不同的任务,比如单CPU核心下执行多线程并非是同时执行多个任务,如果你开两个线程执行,快速交替切换去执行这两个任务,已达到"同时执行效果"。
并行:指应用能够同时执行不同的任务。

创建线程

两种方式创建,一种继承Thread,一种实现Runnable。
事实上创建线程只有一种方式,就是构造一个Thread类,这是创建线程的唯一方式。
运行内容主要来自于两个地方,要么来自于target,要么来自于重写的run方法。

实现Runnable接口比继承Thread类实现线程要好:

  1. 权责分明
    Runnable里只有一个run方法,它定义了需要执行的内容,在这种情况下,实现了Runnable与Thread的解耦,Thread类负责线程启动和属性设置等内容,权责分明。
  2. 在某些情况下可以提高性能
    使用继承Thread类方式,每次执行一次任务,都需要新建一个独立的线程,执行完任务后线程走到生命周期的尽头被销毁,如果还想执行这个任务,就必须再新建一个继承了Thread类。如果我们使用实现Runnable接口的方式,就可以把任务直接传入线程池,使用一些固定的线程来完成任务,不需要每次新建销毁线程,大大降低了性能开销。
  3. 不支持双继承

Callable

带有返回值的Runnable,并且可以抛异常,调用Future的get方法能获取结果,但是会阻塞主进程,可以更改为一段时间内主动查询结果。

public static void callable(){
    Callable<String> callable = new Callable<String>() {
        @Override
        public String call() throws Exception {
            Thread.sleep(500);
            return "Done!";
        }
    };

    ExecutorService executor = Executors.newCachedThreadPool();
    Future<String> future = executor.submit(callable);
    while(true){
        if(future.isDone()){
            try {
                String result = future.get();
                System.out.println("result: "+result);
            } catch (ExecutionException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

线程池

线程池具备的优点:

  • 重用线程池中的线程,避免因为线程的创建和销毁所带来的性能开销。
  • 能有效的控制线程池的最大并发数,避免大量线程之间相互抢占资源而导致的阻塞现象。
  • 能够对线程进行简单管理,并提供定时执行以及指定间隔循环执行等功能。

线程池使用完后需要在onDestroy中进行资源释放,调用shutdown方法。

ThreadPoolExecutor

构造方法如下:

/**
 *  corePoolSize: 核心线程数,运行的线程数小于线程池里的线程数,会额外创建线程来处理任务
 *  maximumPoolSize:线程池允许创建的最大线程数。当核心线程数和缓存队列满了以后,才会根据这个参数创建线程
 *  keepAliveTime:非核心线程闲置的超时时间。超过这个时间则回收。如果设置allowCoreThreadTimeOut属性为true时,keepAliveTime也会应用到核心线程上
 *  workQueue:任务阻塞队列,如果当前线程数大于corePoolSize,则将任务添加到此任务队列中。
 *  ThreadFactory:线程工厂。可以用线程工厂给每个创建出来的线程设置名字。
 *
 *  RejectedExecutionHandler:
 *  ThreadPoolExecutor.AbortPolicy():被拒绝后抛出RejectedExecutionException异常
 *  ThreadPoolExecutor.CallerRunsPolicy():被拒绝后会在线程池当前正在运行的Thread线程池中处理被拒绝的任务。
 *  ThreadPoolExecutor.DiscardOldestPolicy():被拒绝后放弃队列中最旧的未处理的任务,然后将被拒绝的任务添加到等待队列中。
 *  ThreadPoolExecutor.DiscardPolicy():被拒绝后线程池将丢弃被拒绝的任务。
 */
public ThreadPoolExecutor(int corePoolSize,
    int maximumPoolSize,long keepAliveTime,TimeUnit unit,
    BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory, RejectedExecutionHandler handler)
    
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10
    , 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(5), new MyThreadName()
    , new ThreadPoolExecutor.AbortPolicy());

默认情况下核心线程会一直在线程池中存活。也可以设置allowCoreThreadTimeOut,应用在主线程的超时策略,超过这个时长,核心线程就会被终止。
maximumPoolSize是线程池中所能容纳最大线程数,当活动线程数达到这个数值后,后续的新任务将会被阻塞。
workQueue线程池中的任务队列,通过线程池的execute方法提交的Runnable对象会存储在这个参数中。

执行任务的步骤:

  • 如果线程池中的线程数量未达到核心线程的数量,会直接启动一个核心线程来执行任务。
  • 线程池中的线程数量已经达到或者超过核心线程的数量,那么任务会被插入到任务队列中排队等待执行。
  • 执行步骤2,如果无法将任务插入进任务队列,代表任务队列已满,如果线程数量未达到线程池规定的最大值,启动非核心线程来执行任务
  • 执行步骤3,如果线程数量达到线程池规定的最大值,就拒绝执行此任务。会调用RejectedExecutionHandler的RejectedExecutionException方法来通知调用者。

FixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
        0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}

只有核心线程而且这些核心线程不会被回收,任务队列也是没有大小限制的。能更加快速响应外界的请求。

CachedThreadPool

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
        60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
    }

没有核心线程,线程数无限大,空闲时间60s,所有线程处于活动会创建新线程来处理任务,有空闲线程会用空闲线程处理新任务。任务队列是空集合,导致任何任务都会立刻执行。SynchronousQueue是一个无法存储元素的队列。
适合执行大量耗时较少的任务,线程中的所有线程都会超时停止,所以也不占用系统资源。

ScheduledThreadPool

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}

核心线程是固定的,非核心线程是无穷大的。主要执行定时任务和具有固定周期的重复任务。

SingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
    (new ThreadPoolExecutor(1, 1, 0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}

只有一个核心线程,确保所有任务都在同一个线程中按顺序执行。使得这些任务不需要处理线程同步问题。

SingleThreadScheduledExecutor

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
    return new DelegatedScheduledExecutorService
        (new ScheduledThreadPoolExecutor(1));
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

只有一个核心线程,非核心线程是无穷大的。

ForkJoinPool

这个线程池是在JDK 7加入的。
与其他线程池的不同之处:

  1. 任务拆分和汇总
    主任务需要执行非常繁重的计算任务,我们就可以把计算拆分成三个部分,这三个部分是互不影响相互独立的,这样就可以利用CPU的多核优势,并行计算,然后将结果进行汇总。这里面主要涉及两个步骤,第一步是拆分也就是Fork,第二步是汇总也就是Join。
  2. 每个线程都有自己独立的任务队列
    一旦线程中的任务被Fork分裂了,分裂出来的子任务放入线程自己的deque里,而不是放入公共的任务队列中。如果此时有三个子任务放入线程t1 的deque队列中,对于线程t1而言获取任务的成本就降低了,可以直接在自己的任务队列中获取而不必去公共队列中争抢也不会发生阻塞,减少了线程间的竞争和切换,是非常高效的。

ForkJoinPool非常适合用于递归的场景,例如树的遍历、最优路径搜索等场景。
打印菲波那切数列

public class Fibonacci extends RecursiveTask<Integer>{
    int n;
    public Fibonacci(int n) {
        this.n = n;
    }
    
    @Override
    protected Integer compute() {
        if(n <= 1) {
            return n;
        }
        
        Fibonacci f1 = new Fibonacci(n-1);
        f1.fork();
        Fibonacci f2 = new Fibonacci(n-2);
        f2.fork();
        return f1.join()+f2.join();
    }
}

ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask task = forkJoinPool.submit(new Fibonacci(10));
System.out.println(task.get());

阻塞队列

1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不满。
2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。

线程池方法

提交任务:
void execute(Runnable command)
Future submit(Runnable task),有返回结果。
关闭线程池:
shutdown

合理配置线程池

任务特性有关:
CPU密集型任务、IO密集型任务和混合型任务。

  1. CPU密集型任务:当前任务完全依赖于CPU,大量计算型的任务。
    线程数:不要超过机器上CPU同时运行的线程个数。
    核心数和线程数一般锁1:1,但Intel引入超线程技术后,使核心数与线程数形成1:2的关系。
    Runtime.getRuntime().availableProcessors方法获得当前设备的CPU个数。
  2. IO密集型任务:网络通信,磁盘读写。
    线程数:2*当前设备的CPU个数。
  3. 混合型:
    只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。

并发编程的三个问题

  • 原子性
    即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
    要是写入的途中被中断,再读就有问题了。
  • 有序性
    程序执行的顺序按照代码的先后顺序执行。
    处理器为了提高程序运行效率,会对输入代码进行优化,不保证程序中各个语句的执行先后顺序,但会保证代码最后结果会跟顺序执行是一致的。
    虽然不会影响到单个线程的运行,但是会影响线程并发执行顺序。
  • 可见性
    指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。
    也就是一个线程修改的结果,另一个线程马上就能看到。
    普通线程修改值后,不会立即写入主存,会写进缓存区,何时存入也是不确定的。

volatile关键字

  1. 保证了不同线程对这个变量的可见性
  2. 禁止进行指令重排序,但不能保证这个方法有序性
  3. 并不能保证原子性
x = 2;  //语句1
y = 0; //语句2
flag = true; //语句3(这个是volatile进行修饰)
x = 4; //语句4
y = -1; //语句5
只能保证执行语句三的时候,语句1和2已经执行完。但是不能保证1和2的执行顺序,4和5的执行顺序。

volatile修饰的变量如果是对象或数组之类的,其含义是对象获数组的地址具有可见性,但是数组或对象内部的成员改变不具备可见性。

线程安全

资源问题,在写资源没写完的时候,别人也在读写数据导致出现问题的情况。

Synchronized

同步性和互斥性。

private int x = 0;
private int y = 0;
private String name;

private synchronized void count(int newValue){
    x = newValue;
    y = newValue;
}

private synchronized void setName(String newName){
    name = newName;
}

注意一个类中两个方法都使用synchronized关键字进行修饰,这个类就使用了同一个监听器,一个线程正在访问count方法,另一个线程连setName方法都不能访问。保证了两个方法互斥访问。
可以用两个不同的锁对象分别锁住这两个方法,一个线程正在访问count方法,另一个线程可以访问setName方法。

ReentrantLock

可重入锁
如果在上锁和释放锁之间出现异常,会导致一直没办法释放锁,从而出现死锁。所以上锁的代码要加try...finally,在finally写释放锁操作。

ReentrantReadWriteLock

只允许一个线程写入,允许多个线程在没有写入时同时读取,适合读多写少的场景。读读共享,读写互斥,写写互斥。多个线程读可以并行执行。

public class Counter {
    private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
    private final Lock rlock = rwlock.readLock();
    private final Lock wlock = rwlock.writeLock();
    private int[] counts = new int[10];

    public void inc(int index) {
        wlock.lock(); // 加写锁
        try {
            counts[index] += 1;
        } finally {
            wlock.unlock(); // 释放写锁
        }
    }

    public int[] get() {
        rlock.lock(); // 加读锁
        try {
            return Arrays.copyOf(counts, counts.length);
        } finally {
            rlock.unlock(); // 释放读锁
        }
    }
}

Synchronized和ReentrantLock对比

  • Lock使用起来比较灵活,但是必须有释放锁的配合动作。
  • Lock必须手动获取与释放锁,而synchronized不需要手动释放和开启锁。
  • Lock只适用于代码块锁,而synchronized可用于修饰方法、代码块等。
    ReentrantLock提供了一个构造函数,能够控制锁是否是公平的。先获取锁的线程能够先拿到锁,那么这个锁是公平的,反之,是不公平的。synchronized是非公平的。假设B线程挂起状态,A线程刚好释放锁,C线程创建,公平锁必须严格B先获取锁,再执行C。B从挂起状态唤醒到运行阶段需要上下文切换,一次上下文切换大概需要20000个时间周期,假设C执行任务也只需要20000个时间周期,就会影响效率。非公平锁会尽可能减少上下文切换时间。
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
  • Synchronized在线程发生异常时会自动释放锁,因此不会发生异常死锁。Lock异常时不会自动释放锁,所以需要在finally中实现释放锁。
  • 调用interrupt方法,Lock是可以中断线程,调用lockInterruptibly获取锁,Synchronized是不能被中断的,必须等待线程执行完成释放锁。
  • Lock有超时获取锁tryLock,在一定时间内获取不到锁,就返回。

可重入和非可重入区别
递归调用方法,方法被synchronized修饰,非可重入第二次进去获取不到锁。可重入锁第二次进去能获取到锁,+1,等到结束一层层减一,减到0代表递归调用完成了。
synchronized和ReentrantLock都是可重入锁。

死锁

死锁例子

当出现多重锁,A线程拿着B线程需要的锁,B线程拿着A线程需要的锁,持续等下去。

private static void lockDemo() {
    final Object a = new Object();
    final Object b = new Object();
    Thread threadA = new Thread(new Runnable() {
        public void run() {
            synchronized (a) {
                try {
                    System.out.println("now i in threadA-locka");
                    Thread.sleep(1000);
                    synchronized (b) {
                        System.out.println("now i in threadA-lockb");
                    }
                } catch (Exception e) {
                    // ignore
                }
            }
        }
    });

    Thread threadB = new Thread(new Runnable() {
        public void run() {
            synchronized (b) {
                try {
                    System.out.println("now i in threadB-lockb");
                    Thread.sleep(1000);
                    synchronized (a) {
                        System.out.println("now i in threadB-locka");
                    }
                } catch (Exception e) {
                    // ignore
                }
            }
        }
    });

    threadA.start();
    threadB.start();
}

如何定位和解决

使用java jdk自带的jps和jstack工具。
使用jps检测当前执行任务的任务号:



可以确定任务进程号是10836,然后执行jstack命令查看当前进程堆栈信息:



可以看到进程存在死锁,两个线程分别在等待对方持有的Object对象。

饥饿和死锁区别

死锁:多个线程互相持有彼此正在等待的锁而又不释放自己持有的锁时产生的死锁。当多个线程在相同资源集合上等待时,也会发生死锁。
饥饿:当线程无法访问它所需要的资源而不能继续执行时,就发生了饥饿现象。引发饥饿最常见的资源就是CPU时钟周期。只要改变了线程的优先级,程序的行为就将与平台相关,并且会导致发生饥饿的风险。

悲观锁和乐观锁

乐观锁:在更新数据之前,会去对比在我修改数据期间,数据有没有被其他线程修改过:如果没被修改过,就说明真的只有我自己在操作,那我就可以正常的修改数据;如果发现数据和我一开始拿到的不一样了,说明其他线程在这段时间内修改过数据,那说明我迟了一步,所以我会放弃这次修改,并选择报错、重试等策略。
悲观锁:从读数据开始加锁,加锁读,处理数据,写数据,再释放锁,别的线程才能对这个资源进行控制。
如果经常出现竞争就需要用悲观锁,放弃一些性能。
悲观锁:synchronized关键字和Lock接口
乐观锁:原子类
多个线程可以同时操作同一个原子变量。

线程终止

线程终止方法

正确的线程终止方法:
interrupt方法只是标记并不是真正的终止。这个标记状态可以通过isInterrupted方法来获取,这个方法可以多次用。这个判断一般在耗时操作之前判断是否结束,如果标记为结束就不进行这个耗时操作。还有一种判断标记状态,Thread的interrupted方法,这个方法只能用一次,第二次使用就会被置为false。
线程中执行sleep方法、wait方法的时候,调用interrupt方法,不会进行标记,sleep方法直接抛InterruptedException异常,同时清除中断信号,将中断标记位设置成false。这个线程需要做结束之前的收尾工作,释放资源和主动终止线程。

错误的线程终止方法:
stop方法真正的终止,已经被抛弃,危险不安全。
而对于suspend方法和resume方法而言,它们的问题在于如果线程调用suspend方法,它并不会释放锁,就开始进入休眠,但此时有可能仍持有锁,这样就容易导致死锁问题,因为这把锁在线程被resume方法之前,是不会被释放的。

自定义取消标志位

不建议使用
使用volatile修饰标记位,适用的场景

class VolatileCanStop implements Runnable {
    private volatile boolean canceled = false;

    @Override
    public void run() {
        int num = 0;

        try {
            while(!canceled && num <= 1000000){
                if(num % 10 == 0){
                    System.out.println(num+"是10的倍数");
                }
                num++;
                Thread.sleep(1);
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileCanStop r = new VolatileCanStop();
        Thread thread = new Thread(r);
        thread.start();
        Thread.sleep(3000);
        r.canceled = true;
    }
}

不适用场景:
run方法里有阻塞调用时会无法很快检测到取消标志,线程必须从阻塞调用返回后,才会检查这个取消标志。

阻塞和中断

线程中断可以在线程内部设置一个中断标识,同时让处于阻塞(如线程调用了thread.sleep、thread.join、thread.wait)的线程抛出InterruptedException中断异常,使线程跳出阻塞状态,也就是会唤醒线程。在抛出异常后会立即将线程的中断标示位清除,即重新设置为false。

死锁和中断

注意:处于死锁状态的线程无法被中断。
synchornized、reentrantLock.lock()获取锁操作造成的阻塞在中断状态下不能抛出InterruptException,即获取锁操作是不能被中断的,要一直阻塞等到到它获取到锁为止。
也就是说如果产生了死锁,则不能被中断。
可以使用超时锁来打破死锁,reentrantLock.tryLock(timeout,unit)在timeout后会抛出InterruptException,唤醒线程做响应操作。
ReentrantLock的lockInterruptibly获取锁,锁可以被中断。

线程常用方法

wait,notify,notifyAll

如果线程调用了对象的wait方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。只有等待另外线程的通知或被中断才会返回.需要注意,调用wait方法后,会释放对象的锁。
当有线程调用了对象的notifyAll方法(唤醒该对象等待池中的所有wait线程)或notify方法(只随机唤醒该对象等待池中的一个wait线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。竞争成功,获得锁,竞争失败,继续留在锁池中等待下一次锁的竞争。

阿里面试:
为什么wait方法和notify方法需要搭配synchonized关键字使用
wait设计思想:
在使用wait方法时,必须把wait方法写在synchronized保护的while代码块中,并始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行wait方法,而在执行wait方法之前,必须先持有对象的monitor锁,也就是通常所说的synchronized锁。

如果不加synchronized进行保护,下面的生产者消费者模型代码就是这样的。

class BlockingQueue{
    Queue<String> buffer = new LinkedList<String>();
    public void give(String data){
        buffer.add(data);   --1
        notify();//Since someone may be waiting in take  --2
    }
    public String take() throws InterruptedException {
        while (buffer.isEmpty()){ -- 3
            wait(); --4
        }
        return buffer.remove();
    }
}

带来的问题:
虽然刚才消费者判断了buffer.isEmpty条件,但真正执行wait方法时,之前的buffer.isEmpty的结果已经过期了。假设这时没有更多的生产者进行生产,消费者便有可能陷入无穷无尽的等待,因为它错过了刚才give方法内的notify的唤醒。也就是代码先执行3,后执行1,2,最后执行4.然后就死锁了。
所以需要使用synchronized构成原子性。

尽可能用notifyAll而并不是notify
一个生产者两个消费者会出现死锁情况。

  1. 现在有三个线程,生产者P1,消费者C1和C2.开始运行的时候,三个都在锁池中等待竞争,假设C1抢到锁了,C1执行时由于没有资源可以消费 调用wait()方法,释放锁并进入等待池。
  2. C2抢到了锁,开始消费,同理,C2也进入了等待池。现在锁池里面只剩下了P1.
  3. P1获得了锁,开始生产,生产完成后,P1开始调用notify()方法唤醒等待池中的C1或者C2,然后P1调用wait()方法释放锁,并进入了等待池。
  4. 假设唤醒的是C1,C1进入锁池并获得锁,消费后notify()方法唤醒了C2,C2进入锁池,C1进入等待池,现在锁池中只有C1。
  5. C1获得了锁,发现没有任何资源可以消费,wait()后释放了锁,进入了等待池,现在三个线程全都在等待池,锁池中没有任何线程。导致死锁!

正是因为notify随机唤醒的特点,导致在多条件的情况下,会导致某些线程永远不会被通知到。稳妥的方式,是使用 notifyAll,让等待中的线程,都有一次再执行的权利。

join

把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join方法,直到线程A执行完毕后,才会继续执行线程B。

private static String name;
    
    private static void set(){
        name = "zhangsan";
    }
    
    private static void print(){
        System.out.println("name: "+name);
    }
    
    public static void main(String[] args) {
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                set();
            }
        });
        thread2.start();
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                // 如果不加这行内容,打印name永远是null
                // 加了这个,thread1会等待thread2先执行完然后再执行
                try {
                    thread2.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                print();
            }
        });
        thread1.start();
    }
}

yield

使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源,所有执行yield的线程有可能在进入到可执行状态后马上又被执行。具体由谁执行由CPU决定。

sleep和wait的区别

相同点:

  • 它们都可以让线程阻塞。
  • 它们都可以响应interrupt中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。

不同点:

  • wait方法必须在synchronized保护的代码中使用,而sleep方法并没有这个要求。
  • 在同步代码中执行sleep方法时,并不会释放monitor锁,但执行wait方法时会主动释放monitor锁。
  • sleep方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的wait方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。
  • wait/notify是Object类的方法,而sleep是Thread类的方法。
    这个设计主要因为wait/notify是锁级别的操作,锁属于对象,所以把它们定义在Object类中是最合适。毕竟一个线程设计出来是能持有多把锁,而如果是Thread的类的方法,就没办法一个线程持有多把锁,最多一把。

ThreadLocal

ThreadLocal是线程内部的数据存储类,自己线程存储的数据只有自己才能获取到,其它线程获取不到。

它是如何保证线程隔离的?
每个Thread维护一个ThreadLocalMap,这map的key是ThreadLocal实例本身,value才是真正要存储的变量值。
对于不同的线程,每次获取副本值时,别的线程并不能获取当前线程的副本值,形成副本的隔离,互不干扰.
ThreadLocalMap结构类似于HashMap,负载因子是2/3*len,初始容量是16.

如果我想跟踪一个请求,从接收请求,处理到返回的整个流程,有没有好的办法?
使用ThreadLocal来解决线程内部共享数据的问题.

public class ThreadLocalMessage {
    private  ThreadLocal<KettleRunMessageVo> messages;
    
    private ThreadLocalMessage(){
        messages = new ThreadLocal<KettleRunMessageVo>();
    }
    
    public  KettleRunMessageVo getMessage() {
        return messages.get();
    }

    public  void setMessage(KettleRunMessageVo message) {
        messages.set(message);
    }
    
    private static ThreadLocalMessage threadLocalMessage = new ThreadLocalMessage();
    
    public static ThreadLocalMessage getInstance(){
        return threadLocalMessage;
    }
}
//在线程中使用
KettleRunMessageVo message = ThreadLocalMessage.getInstance().getMessage();
if(message==null){
    message = new KettleRunMessageVo();
    ThreadLocalMessage.getInstance().setMessage(message);
}

使用message的set方法设置相应的值.
在同一个线程中的其他地方get这个message得到相应的值.

为什么key要使用弱引用?
避免内存泄漏



当ThreadLocal对象不再使用时,不会因为被ThreadLocalMap引用而造成内存泄露。
查看ThreadLocalMap类中,get,set,remove等相关方法,会发现最后都会调用一个expungeStaleEntry(int staleSlot)方法。
该方法会遍历map中的元素,检查Entity不为空但是Key为空的元素,并清除。

ThreadLocal中还提供了remove方法来清除相关的Entry。

常考点

剩下的没有列出来的点

  • 常考点还有synchronized和volatile关键字区别,这个从虚拟机指令角度答。
  • run和start开启线程方法的区别
    start方法让一个线程进入就绪队列等待分配cpu,分到cpu后才调用实现的run方法,start方法不能重复调用。
    而run方法是业务逻辑实现的地方,本质上和任意一个类的任意一个成员方法并没有任何区别,可以重复执行,可以被单独调用。

参考
Java面试必问-死锁终极篇
《Java并发编程78讲》专栏

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 1.GCD队列有哪几种类型?有哪几种队列? GCD队列分为串行队列、并行队列两种类型;队列有主串行队列、全局并行队...
    oc123阅读 1,787评论 0 7
  • 写在前面的话 ReactiveCocoa 5.0 看了两天,真是看的要吐血,网上基本上没有中文的文章,然后就只能看...
    d4d98020ef88阅读 1,233评论 2 9
  • 线程基础 1、进程和线程的,并行和并发的区别   线程是计算机进行运算调用的最小单元,包含在进程内。例如:一个微信...
    OPice阅读 1,437评论 2 7
  • 一、概述 1. 线程 线程允许在同一个进程中存在多个程序控制流。线程可以共享进程的资源,但是每个线程都有自己的程序...
    Lynn_R01612x2阅读 17,019评论 2 5
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,583评论 16 22