# 一、synchronized 与 Lock 有什么异同

Java语言中提供了两种锁机制的实现对某个共享资源的同步:Synchronized 和 Lock 。其中 Synchronized 使用 Object 类对象本身的 notify()、wait()、notifyAll() 调度机制,而 Lock 使用 Condition 包进行线程之间的调度,完成 Synchronized 实现的所有功能
【1】用法不一样: synchronized 既可以加在方法上,也可以加在特定的代码块中,括号中表示需要的锁对象。而 Lock 需要显式的指定起始位置和终止位置。synchronized 是托管给 JVM 执行的,而 Lock 的锁定是通过代码实现,它有比 synchronized 更精确的线程语义。
【2】性能不一样: 在 JDK5 中增加了一个 Lock 接口的实现类 ReentrantLock。它不仅拥有和 synchronized 相同的并发性和内存语义、还多了锁投票、定时锁、等候锁和中断锁。它们的性能在不同的情况下会有所不同。在资源竞争不激烈的情况下,Synchronized 的性能要优于 RenntrantLock,但是资源竞争激烈的情况下,Synchronized 性能会下降的非常快【使用了重量级锁,通过操作系统的互斥锁实现的】,而ReentrantLock 的性能基本保持不变。
【3】锁机制不一样: Synchronized 获得锁和释放锁的方式都是在块结构中,当获取多个锁时,必须以相反的顺序释放,并且自动解锁,而 Condition 中的 await()、signal()、signalAll() 能够指定要释放的锁。不会因为异常而导致锁没有被释放从而引发死锁的问题。而 Lock 则需要开发人员手动释放,并且必须放在 finally 块中释放,否则会引起死锁问题。此外,Lock 还提供了更强大的功能,他的 tryLock() 方法可以采用非阻塞的方式去获取锁。
【4】DUMP分析的优势: 因为Lock类只是一个普通类,JVM无从得知Lock对象的占用情况,所以在线程DUMP中,也不会包含关于Lock的信息, 关于死锁等问题,就不如用synchronized的编程方式容易识别。

WARNING

虽然 synchronized 与 Lock 都可以实现多线程的同步,但是最好不要同时使用这两种同步机制给统一共享资源加锁(不起作用),因为 ReentrantLock 与 synchronized 所使用的机制不同,所以它们运行是独立的,相当于两个种类的锁,在使用的时候互不影响。

面试题:
【1】当一个线程进入一个对象的 synchronized() 方法后,其他线程是否能够进入此对象的其他方法? 答案:其他线程可进入此对象的非 synchronized 修饰的方法。如果其他方法有 synchronized 修饰,都用的是同一对象锁,就不能访问。
【2】如果其它方法是静态方法,且被 synchronized 修饰,是否可以访问? 答案:可以的,因为 static 修饰的方法,它用的锁是当前类的字节码,而非静态方法使用的是 this,因此可以调用。

# 二、如何实现 Java多线程

Java虚拟机允许应用程序并发地运行多个线程,在 Java语言中实现多线程的方法有三种,其中前两种为常用方法:
【1】继承 Thread类,重写 run()方法: Thread本质上也是实现了 Runnable接口的一个实例,它代表一个线程的实例,并且启动线程的唯一方法就是通过 Thread类的 start()方法,start() 方法是一个本地(native)方法,它将启动一个新的线程,并执行 run() 方法(执行的是自己重写了Thread类的 run()方法),同时调用 start() 方法并不是执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行多线程代码由操作系统决定。

class MyThread extends Thread{//创建线程类
    public void run(){
       System.out.println("Thread Body");//线程的函数体
    }
}

public class Test{
   public static void main(String[] args){
     MyThread thread = new Thread
     thread.run();//开启线程
   }
}
1
2
3
4
5
6
7
8
9
10
11
12

【2】实现 Runnable 接口,并实现该结构的 run() 方法:
 1)自定义实现 Runnable 接口,实现 run() 方法。
 2)创建 Thread 对象,用实现 Runnable 接口的对象作为参数实例化该 Thread 对象。
 3)调用 Thread 的 start() 方法。

class MyThread implements Runnable{
   pulic void run(){
      System.out.println("Thread Body");
   }
}

public class Test{
  public static void main(String[] args){
     MyThread myThread = new MyThread;
     Thread thread = new Thread(myThread);
     thread.start();//启动线程
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

其实,不管是哪种方法,最终都是通过 Thread 类的 API 来控制线程。

【3】实现 Callable 接口,重写 call() 方法: Callable 接口实际是属于 Executor 框架中的功能类,Callable 结构与 Runnable接口的功能类似,但提供了比 Runnable 更强大的功能,主要体现在如下三点:
 1)、Callable 在任务结束后可以提供一个返回值,Runnable 无法提供该功能。
 2)、Callable 中的 call() 方法可以抛出异常,而 Runnable 中的 run() 不能抛出异常。
 3)、运行 Callable 可以拿到一个 Future 对象,Future 对象表示异步计算的结果,它提供了检查计算是否完成的方法。由于线程输入异步计算模型,因此无法从别的线程中得到函数的返回值,在这种情况下,就可以使用 Future 来监控目标线程来调用call() 方法的情况,当调用 Future 的 get() 方法以获取结果时,当前线程会阻塞,直到目标线程的 call() 方法结束返回结果。

public class CallableAndFuture{
   //创建线程类
   public static class CallableTest implements Callable{
     public String call() throws Exception{
        return "Hello World!";
     }
   }
   public static void main(String[] args){
     ExecutorService threadPool = Executors.newSingleThreadExecutor();
     Future<String> future = threadPool.submit(new CallableTest());
     try{
          System.out.println("waiting thread to finish");
          System.out.println(future.get());
        }catch{Exception e}{
          e.printStackTrace
        }
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

输出结果如下:

waiting thread to finish                
Hello World!
1
2

WARNING

当需要实现多线程时,一般推荐使用 Runnable 接口方式,因为 Thread 类定义了多种方法可以被派生类使用或重写,但是只有 run() 方法必须被重写,在 run() 方法中实现这个线程的主要功能,这当然也是实现 Runnable 接口所需的方法。再者,我们很多时候继承一个类是为了去加强和修改这个类才去继承的。因此,如果我们没有必要重写 Thread 类中的其他方法,那么通过继承 Thread 类和实现 Runnable 接口的效果是相同的,这样的话最好还是使用 Runnable 接口来创建线程。

【引申】:一个类是否可以同时继承 Thread 类和实现 Runnable 接口?答案是可以的。

public class Test Extend Thread implements Runnable{
   public static void main(String[] args){
      Thread thread = new Thread(new Test);
      thread.start();
   }
}
1
2
3
4
5
6

如上,Test 实现了 Runnable 接口,但是并没有实现接口的 run() 方法,可能会认为有编译错误,但实际是可以编译通过的,因为 Test 从 Thread 中继承了 run() 方法,这个继承的 run() 方法被当做 Runnable 接口的实现,因此是可以编译通过的,当然也可以自己重写,重写后再调用 run() 方式时,那就是自己重写后的方法了。

# 三、run()方法与 start()方法有什么区别

通常,系统通过调用线程类的 start() 方法启动一个线程,此时该线程处于就绪状态,而非运行状态,也就意味着这个线程可以被 JVM调用执行,执行的过程中,JVM通过调用目标类的 run() 方法来完成实际的操作,当 run() 方法结束后,线程也就会终止。

如果直接调用线程类的 run() 方法,就会被当做一个普通函数调用,程序中仍然只有一个主程序,也就是说 start() 方法能够异步调用 run() 方法,但是直接调用 run() 方法却是同步的,也就无法达到多线程的目的。

# 四、多线程数据同步实现的方法有哪些

当使用多线程访问同一数据时,非常容易出现线程安全问题,因此采用同步机制解决。Java提供了三种方法:

【1】synchronized 关键字: 在Java语言中,每个对象都有一个对象锁与之相关联,该锁表明对象在任何时候只允许被一个线程所拥有,当一个线程调用对象的 synchronize 代码时,需要先获取这个锁,然后再去执行相应的代码,执行结束后,释放锁。synchronize 关键字主要有两种用法(synchronize 方法和 synchronize 代码块)
 1)、synchronized 方法:在方法的声明前加 synchronize 关键字: 将需要对同步资源的操作放入 test() 方法中,就能保证此资源在同一时刻只能被一个线程调用,从而保证资源的安全性。然而当此方法体规模非常大时,会影响系统的效率。

public synchronize void test();
1

 2)、synchronized 代码块:既可以把任意的代码段声明为 synchronized,也可以指定上锁的对象,有非常高的灵活性。

synchronized(syncObject){
     //访问syncObject的代码块
}
1
2
3

【2】wait() 方法与 notify() 方法: 当使用 synchronized 来修饰某个共享资源时,如果线程A1在执行 synchronized 代码,线程A2也要执行此 synchronize 的代码,线程A2将要等到线程A1执行完后执行,这种情况可以使用 wai() 和 notify()。必须是统一把锁,才生效。

class NumberPrint implements Runnable{  
    private int number;  
    public byte res[];  
    public static int count = 5;  
    public NumberPrint(int number, byte a[]){  
        this.number = number;  
        res = a;  
    }  
    public void run(){  
        synchronized (res){  
            while(count-- > 0){  
                try {  
                    res.notify();//唤醒等待res资源的线程,把锁交给线程(该同步锁执行完毕自动释放锁)  
                    System.out.println(" "+number);  
                    res.wait();//释放CPU控制权,释放res的锁,本线程阻塞,等待被唤醒。  
                    System.out.println("------线程"+Thread.currentThread().getName()+"获得锁,wait()后的代码继续运行:"+number);  
                } catch (InterruptedException e) {  
                    // TODO Auto-generated catch block  
                    e.printStackTrace();  
                }  
            }//end of while  
            return;  
        }//synchronized  
          
    }  
}  
public class WaitNotify {
    public static void main(String args[]){  
        final byte a[] = {0};//以该对象为共享资源  
        new Thread(new NumberPrint((1),a),"1").start();  
        new Thread(new NumberPrint((2),a),"2").start();  
    }  
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

输出结果:

1  
2  
-----线程1获得锁,wait()后的代码继续运行:1  
1  
-----线程2获得锁,wait()后的代码继续运行:2  
2  
-----线程1获得锁,wait()后的代码继续运行:1  
1  
-----线程2获得锁,wait()后的代码继续运行:2 
1
2
3
4
5
6
7
8
9

【3】Lock: JDK5 新增加 Lock 接口以及它的一个实现类 ReentrantLock(重入锁),也可以实现多线程的同步;
 1)、lock():以阻塞的方式获取锁,也就是说,如果获取到了锁,就会执行,其他线程需要等待,unlock() 锁后别的线程才能执行,如果别的线程持有锁,当前线程等待,直到获取锁后返回。

public int consume(){
    int m = 0;
    try {
        lock.lock();
        while(ProdLine.size() == 0){
            System.out.println("队列是空的,请稍候");
            empty.await();
        }
        m = ProdLine.removeFirst();
        full.signal(); 
    } catch (InterruptedException e) {
        e.printStackTrace();
    }finally{
        lock.unlock();
        return m;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

 2)、tryLock():以非阻塞的方式获取锁。只是尝试性地去获取一下锁,如果获取到锁,立即返回true,否则,返回false
 3)、tryLock(long timeout,TimeUnit unit):在给定的时间单元内,获取到了锁返回true,否则false
 4)、lockInterruptibly():如果获取了锁,立即返回;如果没有锁,当前线程处于休眠状态,直到获取锁,或者当前线程被中断(会收到InterruptedException异常)。它与 lock() 方法最大的区别在于如果 lock() 方法获取不到锁,就会一直处于阻塞状态,且会忽略 Interrupt() 方法。

# 五、sleep() 方法与 wait() 方法有什么区别

sleep() 是使线程暂停执行一段时间的方法。wait() 也是一种使线程暂停执行的方法,直到被唤醒或等待时间超时

区别:
 1)、原理不同:sleep() 方法是 Thread 类的静态方法,是线程用来控制自身流程的,它会使此线程暂停执行一段时间,而把执行机会让给其他线程,等到时间一到,此线程会自动 “苏醒”。wait() 方法是 Object 类的方法,用于线程间通讯,这个方法会使当前线程拥有该对象锁的进程等待,直到其他线程调用 notify() 方法(或 notifyAll 方法)时才“醒”来,不过开发人员可可以给它指定一个时间,自动“醒”来。与 wait() 方法配套的方法还有 notify() 和 notifyAll() 方法。
 2)、对锁的处理机制不同:由于 sleep() 方法的主要作用是让线程暂停执行一段时间,时间一到则自动恢复,不涉及线程间的通讯,因此,调用 sleep() 方法并不会释放锁。而 wait() 方法则不同,调用后会释放掉他所占用的锁,从而使线程所在对象中的其他 Synchronized 数据可被别的线程使用。
 3)、使用区域不同:由于 wait() 的特殊意义,因此它必须放在同步控制方法或者同步代码块中使用,而 sleep() 则可以放在任何地方使用。
 4)、sleep() 方法必须捕获异常,而wait()、notify()、notifyAll()不需要捕获异常:在 sleep 的过程中,有可能被其他对象调用它的 interrupt(),产生 InterruptedException 异常。

sleep 不会释放“锁标志”,容易导致死锁问题的发生,因此,一般情况下,不推荐使用 sleep() 方法。而推荐使用 wait() 方法

# 六、sleep() 与 yield() 的区别

【1】sleep() 给其他线程运行机会时,不考虑线程的优先级,因此会给低优先级的线程以运行的机会,而 yield() 方法只会给相同优先级或更高优先级的线程以运行的机会。

【2】sleep() 方法会转入阻塞状态,所以,执行 sleep() 方法的线程在指定的时间内不会被执行,而 yield() 方法只是使当前线程重新回到可执行状态,所以执行 yield() 方法的线程很可能在进入到可执行状态后马上又被执行。

# 七、终止线程的方法有哪些

【1】stop() 方法:它会释放已经锁定的所有监视资源,如果当前任何一个受监视资源保护的对象处于不一致的状态(执行了一部分),其他线程将会获取到修改了的部分值,这个时候就可能导致程序执行结果的不确定性,并且这种问题很难被定位。

【2】suspend() 方法:容易发生死锁。因为调用 suspend() 方法不会释放锁,这就会导致此线程挂起。

鉴于以上两种方法的不安全性,Java语言已经不建议使用以上两种方法来终止线程了。

【3】一般建议采用的方法是让线程自行结束进入 Dead 状态。一个线程进入 Dead 状态,既执行完 run() 方法,也就是说提供一种能够自动让 run() 方法结束的方式,在实际中,我们可以通过 flag 标志来控制循环是否执行,从而使线程离开run方法终止线程

public class MyThread implements Runnable{
    private volatile Boolean flag;
    public void stop(){
        flag=false;
    }
    public void run(){
        while(flag);//do something
    }
}
1
2
3
4
5
6
7
8
9

上述通过 stop() 方法虽然可以终止线程,但同样也存在问题;当线程处于阻塞状态时(sleep()被调用或wait()方法被调用或当被I/O阻塞时),上面介绍的方法就不可用了。此时使用 interrupt()方法来打破阻塞的情况,当 interrupt()方法被调用时,会跑出 interruptedException异常,可以通过在 run() 方法中捕获这个异常来让线程安全退出。

public class MyThread implement Runnable{
    public static void main(String[] args){
        Thread thread = new Thread(new MyThread);
        public void run(){
            System.out.println("thread go to sleep");
            try{
                //用休眠来模拟线程被阻塞
                Thread.sleep(5000);
                System.out.println("thread finish");
            } catch (InterruptedException e){
                System.out.println("thread is interrupted!);
            }
        } 
    }
}
thread.start();
therad.interrupt();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

程序运行结果:

thread go to sleep
thread is interrupted!
1
2

如果 I/O 停滞,进入非运行状态,基本上要等到 I/O 完成才能离开这个状态。或者通过抛发异常,使用 readLine() 方法在等待网络上的一个信息,此时线程处于阻塞状态,让程序离开 run() 就使用 close() 方法来关闭流,这个时候就会跑出 IOException 异常,通过捕获此异常就可以离开 run()。

# 八、什么是守护线程

Java提供了两种线程:守护线程和用户线程。守护线程又被称为“服务进程”、“精灵线程”、“后台线程”,是指在程序运行时在后台提供一种通用服务的线程,这种线程并不属于程序中不可或缺的部分,通俗点讲,每一个守护线程都是 JVM 中非守护线程的“保姆”。典型例子就是“垃圾回收器”。只要 JVM 启动,它始终在运行,实时监控和管理系统中可以被回收的资源。

用户线程和守护线程几乎一样,唯一的不同就在于如果用户线程已经全部退出运行,只剩下守护线程运行,JVM 也就退出了因为当所有非守护线程结束时,没有了守护者,守护线程就没有工作可做,也就没有继续运行程序的必要了,程序也就终止了,同时会“杀死”所有的守护线程。也就是说,只要有任何非守护线程运行,程序就不会终止。

Java语言中,守护线程优先级都较低,它并非只有 JVM 内部提供,用户也可以自己设置守护线程,方法就是在调用线程的 start()方法之前,设置 setDaemon(true) 方法,若将参数设置为 false,则表示用户进程模式。需要注意的是,守护线程中产生的其它线程都是守护线程,用户线程也是如此。

class ThreadDemo extends Thread{
 public void run(){
   System.out.println(Thread.currentThread().getName()+":begin");
   try{
       Thread.sleep(1000); 
      }catch(InterruptedException e){
        e.printStackTrace();
      }
    System.out.println(Thread.currentThread().getName()+":end");
  }  
}
1
2
3
4
5
6
7
8
9
10
11

定义一个守护线程:

public class Test{
    public static void main(String[] args){
        System.out.println("test3:begin");
        Thread thread = new ThreadDemo();
        thread.setDaemon(true);
        thread.start();
        System.out.println("test3:end");
    }
}
1
2
3
4
5
6
7
8
9

程序运行结果:

test3:begin
test3:end
Thread-0:begin
1
2
3

从运行结果上可以发现,没有输出 Thread-0:end。之所以是这样的结果,是在启动线程前,将其设置成为了守护线程了,当程序中只有守护线程时,JVM 是可以退出的,也就是说,当 JVM 中只有守护线程运行时,JVM会自动关闭。因此,当 test3 方法调用结束后,mian 线程将退出,此时线程 thread 还处于休眠状态没有运行结果,但是由于此时只有这个守护线程在运行,JVM将会关闭,因此不会输出:“Thread-0:end”。

# 九、join() 方法的作用是什么

在 Java语言中,join() 方法的作用是让调用该方法的线程在执行完 run() 方法后,再执行 join() 方法后面的代码。简单点说就是将两个线程合并,并实现同步功能。具体而言,可以通过线程A的 join() 方法来等待线程A的结束,或者使用线程A的 join(2000) 方法来等待线程A的结束,但最多只等 2s。多线程示例如下:

class ThreadImp implements Runnable{
  public void run(){
    try{
       System.out.println("Begin ThreadImp");
       Thread.sleep(5000);
       System.out.println("End ThreadImp");
    }catch(InterruptedException e){
       e.printStackTrace();
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11

主函数示例如下:

public class JoinTest{
  public static void main(String[] args){
    Thread t = new Thread(new ThreadImp());
    t.start();
    try{
         t.join(1000);//主线程等待1s
         if(t.isAlive()){
            System.out.println("t has not finished");
         }else{
            System.out.println("t has finished");
         }
       System.out.println("joinFinish");
    }catch(InterruptedExcetion e){
       e.printStackTrace();
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

运行结果:

Begin ThreadImp
t has not finished
joinFinish
End ThreadImp
1
2
3
4

# 十、Volatile 关键字

Volatile 是 Java 虚拟机提供的轻量级同步机制。具有如下三种特性:
1)、保证可见性:被 Volatile 修改的变量会被存储在主内存中,而不是工作内存中。可以被其他线程共享访问。
2)、不保证原子性:既对修饰的变量的操作是可分割的(举例:i++ 十次得到的结果,不一定是10),也就是线程不安全的。
3)、禁止指令重排:计算机会对编译后的 java 语言进行编译。为了提高运行效率,会对指令进行重排。单线程重排后,是没有问题的,但是在多线程中重排就会影响执行结果。

# 十一、Synchronized 的实现原理以及锁优化

Java 虚拟机中的同步(Synchronization)基于进入和退出 Monitor对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法表结构的 ACC_SYNCHRONIZED 标志来隐式实现的。

同步代码块: monitorenter 指令插入到同步代码块的开始位置,monitorexit 指令插入到同步代码块的结束位置,JVM需要保证每一个 monitorenter都有一个 monitorexit与之相对应。任何对象都有一个 monitor与之相关联,当且一个 monitor被持有之后,他将处于锁定状态。线程执行到 monitorenter指令时,将会尝试获取对象所对应的 monitor所有权,即尝试获取对象的锁; Java虚拟机对 synchronize的优化:锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级,关于重量级锁,前面我们已详细分析过,下面我们将介绍偏向锁和轻量级锁以及JVM的其他优化手段。

# 十二、分段锁的原理,锁力度减小的思考

segment 分段锁使用场景最熟悉的应该属于 concurrentHashMap在 JDK1.7 的时候用来保证线程安全。那么主要看下其源码即可:链接

# 十三、LockSupport 工具

当需要阻塞或唤醒一个线程时,都会使用 LockSupport 工具类来完成响应工作。LockSupport 定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而 LockSupport 也成为构建同步组件的基本工具。LockSupport 定义了一组以 park 开头的方法用来阻塞当前线程,以及 unpark(Thread thread) 方法唤醒一个被阻塞的线程。

方法名称 描述
void park() 阻塞当前线程,如果调用unpark(Thread thread)方法或者当前线程被中断interrupt(),才能从 park() 方法返回。
void parkNanos(long nanos) 阻塞当前线程,最长不超过 nanos 纳秒,返回条件在 park() 基础上增加了超时返回。
void parkUntil(long deadline) 阻塞当前线程,直到 deadline时间(从 1970年开始到 deadline 时间的毫秒数)。
void unpark(Thread thread) 唤醒处于阻塞状态的线程 thread

在 JDK 6中,LockSupport 增加了 park(Object blocker)、parkNanos(Object blocker,long nanos) 和 parkUntil(Object blocker,long deadline) 三个方法,用于实现阻塞当前线程的功能,其中参数 blocker 是用来标识当前线程在等待的对象(以下称为阻塞对象),该对象主要用于问题排查和系统监控。有阻塞对象的 park 方法能够传递给开发人员更多的现场信息。这是由于 Java5 之前,当线程阻塞(使用 synchronized 关键字)在一个对象上时,通过线程 dump 能够查看到线程的阻塞对象,方便问题定位,而在 Java5 推出的 Lock 等并发工具时却遗漏了这一点,即使在线程 dump 时无法提供阻塞对象的信息。因此,在 Java6 中,LockSupport 新增了有阻塞对象的park() 方法。

# 十四、ThreadLocal原理

ThreadLocal 也叫线程本地变量。每一个 ThreadLocal能够放一个线程级别的变量,可是它本身能够被多个线程共享使用,并且又能够达到线程安全的目的,且绝对线程安全。ThreadLocal中的变量是线程分离的,别的线程无法使用,保证了变量的安全性。

public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();
1

【ThreadLoacal 原理】: 首先 ThreadLocal 是一个泛型类,保证可以接受任何类型的对象。因为一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个 ThreadLocalMap类对应的 get()、set() 方法。set 方法源码如下:

//设置当前线程的ThreadLocal变量的副本为指定的值
public void set(T value) {
    Thread t = Thread.currentThread()
    //获取当前线程的 ThreadLocalMap 对象(每个线程都有一个 ThreadLocal.ThreadLocalMap对象)如果为空,
    //则创建一个 ThreadLocalMap 对象,否则设置值。
    ThreadLocalMap map = getMap(t);
    if(map !=null)
        map.set(this, value);
    else        
        createMap(t, value);
}

//返回线程的threadLocals变量,它的类型是ThreadLocal.ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

//创建一个 ThreadLocalMap 实例
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

//ThreadLocalMap 底层:是一个数组,数组中元素类型是Entry类型
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

也就是说,每个线程都维护了一个 ThreadLocal.ThreadLocalMap类型的对象,而 set操作其实就是以 ThreadLocal变量为 key,以我们指定的值为 value,最后将这个键值对封装成 Entry对象放到该线程的 ThreadLocal.ThreadLocalMap对象中。每个ThreadLocal 变量在该线程中都是 ThreadLocal.ThreadLocalMap 对象中的一个 Entry。既然每个 ThreadLocal变量都对应ThreadLocal.ThreadLocalMap 中的一个元素,那么就可以对这些元素进行读写删除操作。

get 方法源码如下:就是从当前线程的 ThreadLocal.ThreadLocalMap对象中取出对应的 ThreadLocal变量所对应的值。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}
1
2
3
4
5
6
7
8
9
10

【注意事项】:实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。所以在 finally 中都需要通过 remove()方法。如果线程关闭,Thread类会进行一些清理工作,包括清理ThreadLocalMap。但是如果使用线程池,由于线程可能并不是真正的关闭(比如newFixedThreadPool会保持线程一只存活)。因此,如果将一些大对象存放到 ThreadLocalMap中,可能会造成内存泄漏。因为线程没有关闭,无法回收,但是这些对象不会再被使用了。如果希望及时回收对象,则可以使用Thread.remove()方法将变量移除。

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
1
2
3
4
5

【ThreadLocal使用场景】 最常见的 ThreadLocal使用场景为用来解决数据库连接、Session管理等。

# 十五、线程池如何调优,最大数目如何确认

【1】设置最大线程数: 对于特定硬件上的负载,最大线程数设置为多少最好呢?这个问题回答起来并不简单:它取决于负载特性以及底层硬件。特别是,最优线程数还与每个任务阻塞的频率有关。假设 JVM有4个 CPU可用,很明显最大线程数至少要设置为4。的确,除了处理这些任务,JVM还有些线程要做其他的事,但是它们几乎从来不会占用一个完整的CPU,至于这个数值是否要大于4,则需要进行大量充分的测试。有以下两点需要注意:①、一旦服务器成为瓶颈,向服务器增加负载是非常有害的;②、对于 CPU密集型或 IO密集型的机器增加线程数实际会降低整体的吞吐量;

【2】设置最小线程数: 一旦确定了线程池的最大线程数,就该确定所需的最小线程数了。大部分情况下,开发者会直截了当的将他们设置成同一个值。将最小线程数设置为其他某个值(比如1),出发点是为了防止系统创建太多线程,以节省系统资源。指定一个最小线程数的负面影响相当小。如果第一次就有很多任务要执行,会有负面影响:这是线程池需要创建一个新线程。创建线程对性能不利,这也是为什么起初需要线程池的原因。一般而言,对于线程数为最小值的线程池,一个新线程一旦创建出来,至少应该保留几分钟,以处理任何负载飙升。空闲时间应该以分钟计,而且至少在10分钟到30分钟之间,这样可以防止频繁创建线程。

【3】线程池任务大小: 等待线程池来执行的任务会被保存到某个队列或列表中;当池中有线程可以执行任务时,就从队列中拉出一个。这会导致不均衡:队列中任务的数量可能变得非常大。如果队列太大,其中的任务就必须等待很长时间,直到前面的任务执行完毕。对于任务队列,线程池通常会限制其大小。但是这个值应该如何调优,并没有一个通用的规则。若要确定哪个值能带来我们需要的性能,测量我们的真实应用是唯一的途径。不管是哪种情况,如果达到了队列限制,再添加任务就会失败。ThreadPoolExecutor有一个rejectedExecution方法,用于处理这种情况,默认会抛出RejectedExecutionExecption。应用服务器会向用户返回某个错误:或者是HTTP状态码500,或者是Web服务器捕获异常错误,并向用户给出合理的解释消息,其中后者是最理想的。

【4】设置 ThreadPoolExecutor的大小: 线程池的一般行为是这样的:创建时准备最小数目的线程,如果来了一个任务,而此时所有的线程都在忙碌,则启动一个新线程(一直到达到最大线程数),任务就会立即执行。否则,任务被加入到等待队列,如果队列中已经无法加入新任务,则拒接之。根据所选任务队列的类型,ThreadPoolExecutor会决定何时会启动一个新线程。有以下三种可能:
 ■ SynchronousQueue:如果 ThreadPoolExecutor搭配的是 SynchronousQueue,则线程池的行为和我们预期的一样,它会考虑线程数:如果所有的线程都在忙碌,而且池中的线程数尚未达到最大,则会为新任务启动一个新线程。然而这个队列没办法保存等待的任务:如果来了一个任务,创建的线程数已经达到最大值,而且所有的线程都在忙碌,则新的任务都会被拒绝,所以如果是管理少量的任务,这是个不错的选择,对于其他的情况就不适合了。
 ■ 无界队列:如果 ThreadPoolExecutor搭配的是无界队列,如LinkedBlockingQueue,则不会拒绝任何任务(因为队列大小没有限制)。这种情况下,ThreadPoolExecutor最多仅会按照最小线程数创建线程,也就是说最大线程池大小被忽略了。如果最大线程数和最小线程数相同,则这种选择和配置了固定线程数的传统线程池运行机制最为接近。
 ■ 有界队列:搭配了有界队列,如 ArrayBlockingQueue的 ThreadPoolExecutor会采用一个非常负责的算法。比如假定线程池的最小线程数为4,最大为8所用的 ArrayBlockingQueue最大为10。随着任务到达并被放到队列中,线程池中最多运行4个线程(即最小线程数)。即使队列完全填满,也就是说有10个处于等待状态的任务,ThreadPoolExecutor也只会利用4个线程。如果队列已满,而又有新任务进来,此时才会启动一个新线程,这里不会因为队列已满而拒接该任务,相反会启动一个新线程。新线程会运行队列中的第一个任务,为新来的任务腾出空间。这个算法背后的理念是:该池大部分时间仅使用核心线程(4个),即使有适量的任务在队列中等待运行。这时线程池就可以用作节流阀。如果挤压的请求变得非常多,这时该池就会尝试运行更多的线程来清理;这时第二个节流阀—最大线程数就起作用了。

TIP

对于上面提到的每一种选择,都能找到很多支持或反对的依据,但是在尝试获得最好的性能时,可以应用 KISS原则"Keep it simple,stupid"。可以将最小线程数和最大线程数设置为相同,在保存任务方面,如果适合无界队列,则选择LinkedBlockingQueue;如果适合有界队列,则选择 ArrayBlockingQueue。

# 十六、如何保证多线程下 i++ 结果正确

【1】使用循环CAS+volatile,实现 i++原子操作;
【2】使用 synchronized,实现 i++原子操作;
【3】使用 Lock锁机制,实现i++原子操作;
【4】Semaphore构造方法中传入的参数是1的时候,此时线程并发数最多是1个,即是线程安全的,这种方式也可以做到现场互斥。
【5】使用 AtomicInteger:由硬件提供原子操作指令实现的;

# 十七、AQS

AQS(AbstractQueuedSynchronizer):这个类在 java.util.concurrent.locks包,AbstractQuenedSynchronizer 抽象的队列同步器。是除了 Java自带的 synchronized 关键字之外的锁机制。

AQS的核心思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS是用 CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。注意:AQS 是自旋锁:在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功。

CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。AQS 是将每一条请求共享资源的线程封装成一个 CLH锁队列的一个结点(Node),来实现锁的分配。

# 十八、Java 的信号灯

Semaphore 可以维护当前访问自身的线程个数,并提供了同步机制。使用 Semaphore可以控制同时访问资源的线程个数(即允许n个任务同时访问这个资源),例如,实现一个文件允许的并发访问数。单个信号量的 Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。Java 实现互斥线程同步有三种方式synchronized、lock 、单Semaphore。

public class SemaphoreTest {
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        //第二个参数表示后续的线程是随机获得优先机会,或者按照先来后到的顺序获得机会,
        //这取决于构造Semaphore对象时传入的参数选项。
        final Semaphore semaphore = new Semaphore(1,true);
        for(int i=0;i<10;i++){
            Runnable runnable = new Runnable(){
                    public void run(){
                    try {
                        sp.acquire();
                    } catch (InterruptedException e1) {
                        e1.printStackTrace();
                    }
                    System.out.println("线程" + Thread.currentThread().getName() +
                            "进入,当前已有" + (3-sp.availablePermits()) + "个并发");
                    try {
                        Thread.sleep((long)(Math.random()*10000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程" + Thread.currentThread().getName() +
                            "即将离开");                   
                    sp.release();
                    //下面代码有时候执行不准确,因为其没有和上面的代码合成原子单元
                    System.out.println("线程" + Thread.currentThread().getName() +
                            "已离开,当前已有" + (3-sp.availablePermits()) + "个并发");                   
                }
            };
            service.execute(runnable);         
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 十九、进程之间如何通信

【1】管道: 看一条 Linux 的语句 netstat -anp | grep 8080 学过 Linux 命名的估计都懂这条语句的含义,其中”|“是管道的意思,它的作用就是把前一条命令的输出作为后一条命令的输入。在这里就是把 netstat -anp 的输出结果作为 grep 8080 这条命令的输入。如果两个进程要进行通信的话,就可以用这种管道来进行通信了。并且这种通信方式是单向的,只能把第一个命令的输出作为第二个命令的输入,如果进程之间想要互相通信的话,那么需要创建两个管道。

【2】中间件: 我们可以用消息队列的通信模式来解决这个问题,例如 a 进程要给 b 进程发送消息,只需要把消息放在对应的消息队列里就行了,b 进程需要的时候再去对应的消息队列里取出来。同理,b 进程要个 a 进程发送消息也是一样。这种通信方式也类似于缓存吧。缺点是,如果 a 进程发送的数据占的内存比较大,并且两个进程之间的通信特别频繁的话,消息队列模型就不大适合了。因为 a 发送的数据很大的话,意味发送消息(拷贝)这个过程需要花很多时间来读内存。

【3】共享内存: 共享内存这个通信方式就可以很好着解决拷贝所消耗的时间了。我们都知道,系统加载一个进程的时候,分配给进程的内存并不是实际物理内存,而是虚拟内存空间。那么我们可以让两个进程各自拿出一块虚拟地址空间来,然后映射到相同的物理内存中,这样,两个进程虽然有着独立的虚拟内存空间,但有一部分却是映射到相同的物理内存,这就完成了内存共享机制了。

【4】信号量: 共享内存最大的问题就是多进程竞争内存的问题,就像类似于我们平时说的线程安全问题。如何解决这个问题?这个时候我们的信号量就上场了。信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。例如信号量的初始值是 1,然后 a 进程来访问内存1的时候,我们就把信号量的值设为 0,然后进程b 也要来访问内存1的时候,看到信号量的值为 0 就知道已经有进程在访问内存1了,这个时候进程 b 就会访问不了内存1。所以说,信号量也是进程之间的一种通信方式。

【5】Socket: 上面我们说的共享内存、管道、信号量、消息队列,他们都是多个进程在一台主机之间的通信,那两个相隔几千里的进程能够进行通信吗?答是必须的,这个时候 Socket 这家伙就派上用场了,例如我们平时通过浏览器发起一个 http 请求,然后服务器给你返回对应的数据,这种就是采用 Socket 的通信方式了。

# 二十、interrupt 中断条件

【1】如果线程被 Object.wait、Thread.join 和 Thread.sleep 三种方法之一阻塞或者1.5中的condition.await、以及可中断的通道上的 I/O 操作方法后可进入阻塞状态。此时调用该线程的 interrupt()方法,那么该线程将抛出一个 InterruptedException 中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt() 将不起作用,直到执行到 wait()、sleep()、join()时,才马上会抛出 InterruptedException。 当一个线程运行时,另一个线程可以调用对应的 Thread对象的 interrupt() 方法来中断它,该方法只是在目标线程中设置一个标志,表示它已经被中断,并立即返回。中断的结果线程是死亡还是等待新的任务或是继续运行至下一步,就取决于这个程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(中断标示值是否为true)。它并不像 stop方法那样会中断一个正在运行的线程。

public class Test {
    public static void main(String[] args) throws InterruptedException {
        RunTest rt = new RunTest();
        Thread t = new Thread(rt);
        t.start();
        t.interrupt();
    }
}
 
class RunTest implements Runnable {
    @Override
    public void run() {
        try {
            for (long i = 0; i < 10000000L; i++) {
                System.out.println("I'm doing a time consuming task");
            }
//            TimeUnit.SECONDS.sleep(2); 这条语句会被中断,抛出java.lang.InterruptedException异常
            System.out.println("Thread finished.");
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("Thread execption.");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

RunTest 线程是在循环一个非常大的数字,但它没有阻塞,所以 interrupt()方法对它无效,RunTest线程会被正常执行完,打印Thread finished。

【2】MyThreadA.isInterrupted():判断 Thread 线程是否设置了中断标记,如果该线程已经中断,则返回 true;否则返回 false。需要注意的是,如果线程被中断且已经抛出了异常,它会清除中断标志。

【3】Thread.interrupted():这个是静态方法,直接Thread.interrupted()就好了,它会判断当前线程是否被设置了中断标志,并且清除中断标志。重点:静态方法,当前线程,清除中断标志。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。

1 public static boolean interrupted() {
2     return currentThread().isInterrupted(true);
3 }
1
2
3

interrupt 相关的三个方法就写完了,那么对于线程执行时间太长,但又不是阻塞的情况,该怎么办呢(例子中很大的 for循环)?ThreadPoolExecutor.shutdown()、ThreadPoolExecutor.shutdownNow() 以及 ThreadPoolExecutor.awaitTermination(long timeout, TimeUnit unit),Future.get() 方法都试过了,都不能解决。最后在 shutdownNow() 方法的注释找到了结果。

用博主自己的意思润色翻译为:shutdownNow()方法只能尽可能的尝试去停止正在运行的线程任务,但并不保证一定能成功。比如,此方法的典型实现是通过给线程任务添加一个中断标记 interrupt。但是呢,如果正在运行的线程任务对 interrupt()方法不感冒的话(不属于Object.wait, Thread.join和Thread.sleep阻塞),那么这部分的线程任务是不会停止的。

WARNING

synchronized 在获锁的过程中是不能被中断的,意思是说如果产生了死锁,则不可能被中断(请参考后面的测试例子)。与synchronized 功能相似的 reentrantLock.lock()方法也是一样,它也不可中断的,即如果发生死锁,那么 reentrantLock.lock() 方法无法终止,如果调用时被阻塞,则它一直阻塞到它获取到锁为止。但是如果调用带超时的tryLock方法reentrantLock.tryLock(long timeout, TimeUnit unit),那么如果线程在等待时被中断,将抛出一个 InterruptedException异常,这是一个非常有用的特性,因为它允许程序打破死锁。你也可以调用reentrantLock.lockInterruptibly() 方法,它就相当于一个超时设为无限的 tryLock方法。

【4】 interrupt() 方法的使用:在阅读线程池源码的时候,我们发现关闭线程池的时候就是使用的 interrupt()方法,我们来看一下线程池关闭这块的源码。我们看到很多文章都说,我们在关闭线程池的时候不要直接调用 shutdownNow()方法,而是需要先调用shutdown() 方法接着调用 awaitTermination(long timeout, TimeUnit unit)方法,那到底为什么这样说呢,我们通过源码来分析一下。shutdownNow() 方法的执行逻辑就是将线程池状态修改为 STOP,然后调用线程里的所有线程的 interrupt()方法。

 1 public List<Runnable> shutdownNow() {
 2     List<Runnable> tasks;
 3     final ReentrantLock mainLock = this.mainLock;
 4     mainLock.lock();
 5     try {
 6         checkShutdownAccess();
 7         //原子性的修改线程池的状态为STOP
 8         advanceRunState(STOP);
 9         //遍历线程里的所有工作限次,然后调用线程的interrupt方法。
10         interruptWorkers();
11         //将队列中还没有执行的任务放到列表中,返回给调用方
12         tasks = drainQueue();
13     } finally {
14         mainLock.unlock();
15     }
16     /*在以下情况将线程池变为TERMINATED终止状态
17     * shutdown 且 正在运行的worker 和 workQueue队列 都empty
18     * stop 且  没有正在运行的worker
19     * 
20     * 这个方法必须在任何可能导致线程池终止的情况下被调用,如:
21     * 减少worker数量
22     * shutdown时从queue中移除任务
23     * 
24     * 这个方法不是私有的,所以允许子类ScheduledThreadPoolExecutor调用
25     */
26     tryTerminate();
27     return tasks;
28 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 二十一、Java 通过 Executors工具类创建出来的线程池有什么区别

《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 new ThreadPoolExecutor 实例的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

【Executors 返回线程池对象的弊端如下】:FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。

CachedThreadPool 和 ScheduledThreadPool: 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。

# 二十二、读写锁

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。 一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。正是因为这个特性,当读写锁是写加锁状态时, 在这个锁被解锁之前, 所有试图对这个锁加锁的线程都会被阻塞。

当读写锁在读加锁状态时, 所有试图以读模式对它进行加锁的线程都可以得到访问权, 但是如果线程希望以写模式对此锁进行加锁, 它必须直到所有的线程释放锁。通常, 当读写锁处于读模式锁住状态时, 如果有另外线程试图以写模式加锁, 读写锁通常会阻塞随后的读模式锁请求,这样可以避免读模式锁长期占用,而等待的写模式锁请求长期阻塞。

读写锁适合于对数据结构的读次数比写次数多得多的情况。因为,读模式锁定时可以共享,以写模式锁住时意味着独占,所以读写锁又叫共享-独占锁。

ReadWriteLock 同 Lock一样也是一个接口,提供了 readLock和 writeLock两种锁的操作机制,一个是只读的锁,一个是写锁

// 实际实现类--ReentrantReadWriteLock,默认非公平模式
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

//读
public void get(){
    //使用读锁
    readWriteLock.readLock().lock();
    try {
        System.out.println(Thread.currentThread().getName()+" : "+number);
    }finally {
        readWriteLock.readLock().unlock();
    }
}
//写
public void set(int number){
    readWriteLock.writeLock().lock();
    try {
        this.number = number;
        System.out.println(Thread.currentThread().getName()+" : "+number);
    }finally {
        readWriteLock.writeLock().unlock();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 二十三、有四个线程A、B、C、D、E,现在需要 E线程在 ABCD四个线程结束之后再执行

【1】join 让主线程等待子线程运行结束后再继续运行:join方法中如果传入参数,则表示这样的意思:如果线程A 中掉用线程B的 join(10),则表示线程A 会等待线程B 执行10毫秒,10毫秒过后,A、B线程并行执行。需要注意的是,jdk规定,join(0)的意思不是 线程A等待线程B 0秒,而是线程A 等待线程B 无限时间,直到线程B 执行完毕,即join(0)等价于join()。(其实join()中调用的是join(0))

【2】利用并发包里的 Excutors的 newSingleThreadExecutor产生一个单线程的线程池,而这个线程池的底层原理就是一个先进先出(FIFO)的队列。代码中 executor.submit依次添加了123线程,按照 FIFO的特性,执行顺序也就是123的执行结果,从而保证了执行顺序。

【3】使用 CountDownLatch 控制多个线程执行顺序 cutDown()方法和 await()方法:可以通过调用CounDownLatch对象的cutDown()方法,来使计数减1;如果调用对象上的await()方法,那么调用者就会一直阻塞在这里,直到别人通过cutDown方法,将计数减到0,才可以继续执行。

# 二十四、什么是线程

线程是指程序在运行的过程中,能够执行程序代码的一个执行单元。Java语言中,线程有四种状态:运行、就绪、挂起、结束

# 二十五、线程与进程的区别

进程是指一段正在执行的程序。而线程有时也被称为轻量级进程,它是程序执行的最小单元,一个进程可以拥有多个线程,各个线程之间共享程序的内存空间(代码段、数据段、堆空间)及一些进程级的文件(列如:打开的文件),但是各个线程拥有自己的栈空间。在操作系统级别上,程序的执行都是以进程为单位的,而每个进程中通常都会有多个线程互不影响地并发执行。

# 二十六、为什么要使用多线程

【1】提高执行效率,减少程序的响应时间。因为单线程执行的过程只有一个有效的操作序列,如果某个操作很耗时(或等待网络响应),此时程序就不会响应鼠标和键盘等操作,如果使用多线程,就可以将耗时的线程分配到一个单独的线程上执行,从而使程序具备号更好的交互性。

【2】与进程相比,线程的创建和切换开销更小。因开启一个新的进程需要分配独立的地址空间,建立许多数据结构来维护代码块等信息,而运行于同一个进程内的线程共享代码段、数据段、线程的启动和切换的开销比进程要少很多。同时多线程在数据共享方面效率非常高。

【3】目前市场上服务器配置大多数都是多 CPU或多核计算机等,它们本身而言就具有执行多线程的能力,如果使用单个线程,就无法重复利用计算机资源,造成资源浪费。因此在多 CPU 计算机上使用多线程能提高 CPU 的利用率。

【4】利用多线程能简化程序程序的结构,是程序便于理解和维护。一个非常复杂的进程可以分成多个线程来执行。

# 二十七、同步与异步有什么区别

在多线程的环境中,通常会遇到数据共享问题,为了确保共享资源的正确性和安全性,就必须对共享数据进行同步处理(也就是锁机制)。对共享数据进行同步操作(增删改),就必须要获得每个线程对象的锁(this锁),这样可以保证同一时刻只有一个线程对其操作,其他线程要想对其操作需要排队等候并获取锁。当然在等候队列中优先级最高的线程才能获得该锁,从而进入共享代码区。 Java 语言在同步机制中提供了语言级的支持,可以通过使用 synchronize 关键字来实现同步,但该方法是以很大的系统开销作为代价的,有时候甚至可能造成死锁,所以,同步控制并不是越多越好,要避免所谓的同步控制。实现同步的方法有两种:①、同步方法(this锁)。②、同步代码块(this锁或者自定义锁)当使用 this 锁时,就与同步方法共享同一锁,只有当①释放,②才可以使用。同时,同步代码块的范围也小于同步方法,建议使用,相比之下能够提高性能。

# 二十八、CountDownLatch 和 CyclicBarrier 的用法

【博客链接】:链接

# 二十九、八种阻塞队列以及各个阻塞队列的特性

【博客链接】:链接

# 三十、Condition 接口及其实现原理

【博客链接】:链接

# 三十一、分析线程池的实现原理和线程的调度过程

【博客链接】:链接

# 三十二、线程池的种类,区别和使用场景

【博客链接】:链接

# 三十三、Java 内存模型

【博客链接】:链接

# 三十四、如何检测死锁?怎么预防死锁

【博客链接】:链接

# 三十五、ConcurrenHashMap

【博客链接】:链接

# 三十六、多并发使用的场景

整理中。。。

# 三十七、Fork/Join 框架的理解

【博客链接】:链接

# 三十八、什么是 AQS? 为什么它是核心?

【博客链接】:链接

# 三十九、AQS的核心思想是什么? 它是怎么实现的? 底层数据结构等?

# 四十、AQS有哪些核心的方法?

# 四十一、AQS定义什么样的资源获取方式? AQS定义了两种资源获取方式:独占(只有一个线程能访问执行,又根据是否按队列的顺序分为公平锁和非公平锁,如ReentrantLock) 和共享(多个线程可同时访问执行,如Semaphore、CountDownLatch、 CyclicBarrier )。ReentrantReadWriteLock可以看成是组合式,允许多个线程同时对某一资源进行读。

# 四十二、AQS底层使用了什么样的设计模式?

# 四十三、AQS的应用示例?

(adsbygoogle = window.adsbygoogle || []).push({});