最近在看面试题,所以想用自己的理解总结一下,便于加深印象。
为什么使用多线程
- 使用多线程可以充分利用CPU,提高CPU的使用率。
- 提高系统的运行效率,对于一些复杂或者耗时的功能,可以对其进行拆分,比如将某个任务拆分了A、B、C三个子任务,如果子任务之间没有依赖关系,那么就可以使用多线程同时运行A、B、C三个子任务,来提高效率。
- 可以通过多线程处理一些异步任务。
创线程的方式有哪些
(1)继承Thread方式创建
class MyThread extends Thread {
@Override
public void run() {
System.out.println("run task by Thread");
}
}
启动线程:
public class Test {
public static void main(String[] args) {
// 创建线程
MyThread myThread = new MyThread();
// 启动线程
myThread.start();
}
}
(2)通过实现Runnable的方式创建
class Task implements Runnable{
@Override
public void run() {
System.out.println("run task by Runnable");
}
}
启动线程:
public class Test {
public static void main(String[] args) {
// 创建线程
Thread myTask = new Thread(new Task());
// 启动线程
myTask.start();
}
}
(3)如果需要返回值,还可以使用Callable+FutureTask的方式(也可以通过Callable+Future配合线程池使用):
class Task implements Callable<Integer>{
/**
* 相当于run方法
*/
@Override
public Integer call() throws Exception {
Thread.sleep(5000);
int sum = 0;
for(int i=0; i<10; i++) {
sum += i;
}
return sum;
}
}
运行线程:
public class FutureTest {
public static void main(String[] args) {
// 创建FutureTask
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Task());
// 创建线程
Thread thread = new Thread(futureTask);
// 启动线程
thread.start();
try {
// get()方法可以获取返回结果,它会阻塞到任务执行完毕
System.out.println("运行结果:" + futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
Runnable和Callable的区别
相同点
(1)创建线程通常使用实现接口的方式,面向接口编程,这样在开发过程中可以提高系统的扩展性和灵活性,Runnable和Callable都是接口;
(2)Runnable和Callable都需要通过Thread.start()启动线程;
不同点
(1)Runnable的run方法没有返回值只能返回void,并且不能向上抛出异常,而Callable的call方法既可以有返回值,也可以向上抛出异常。
如何解决线程安全问题
- 提供了syncronized关键字来加锁;
- 提供了Lock接口来加锁;
- volatile关键字,可以保证可见性,需要注意它保证的只是可见性,并不能保证原子性,如果是非原子性的操作,单纯使用volatile并不能保证线程的安全;
- java.util.concurrent包下提供了一系列类,比如一些并发容器ConcurrentHashMap等;
syncronized和Lock的区别
- syncronized是Java中的关键字,而Lock是一个接口;
- syncronized在发生异常时,可以自动释放线程持有的锁,Lock在发生异常时,如果没有通过unLock方法释放锁,则不会自动释放锁,所以在使用Lock的时候注意在finally中调用unLock去释放锁,否则有可能导致死锁的发生;
- Lock调用
lockInterruptibly
在等待抢占某个锁的过程中,可以响应中断,而synchronized不能,会一直等待下去,不能响应中断; - Lock通过tryLock方法可以设置等待时间,在设定的时间内如果未能成功获取锁,将放弃抢占锁,并返回false,如果成功获取锁返回true,通过返回值可以判断是否获取到锁,而syncronized没有这个功能;
syncronized实现原理
syncronized可以加在方法上,对整个方法实现同步访问,也可以使用在代码块中,对某块代码进行同步访问。
(1)普通方法
对于普通方法,锁住的是当前实例对象:
public class SyncTest {
private int i = 0;
public synchronized void add() {
System.out.println("【synchronized】");
i++;
}
}
在编译的字节码中,可以看到方法的flag中有ACC_SYNCHRONIZED
标记:
(2)静态方法
对于静态方法,锁住的是当前类的class对象:
public class SyncTest {
private static int j = 0;
// 静态方法,锁住的是SyncTest.class
public static synchronized void add() {
System.out.println("【static synchronized】");
j++;
}
}
同样在编译的字节码中,可以看到ACC_SYNCHRONIZED
标记:
(3)同步代码块
对于同步代码块,锁住的是括号中的对象:
public class Test {
private int i = 0;
public void add() {
// 锁住的是括号内的对象,这里锁住的是Test.class
synchronized (Test.class) {
i++;
}
}
}
在编译后的字节码中,可以看到添加了monitorenter和monitorexit指令:
每个实例对象和类的Class对象都会关联一个Monitor,Monitor字面翻译为监视器,也可以称之为管程,在Java中就是通过它来实现多线程下的同步访问的。
对于同步代码块,在编译后的字节码中看到添加了monitorenter和monitorexit指令,当执行到monitorenter时,就会去获取锁住对象的Monitor锁,它可以保证在同一时刻只能有一个线程获取到锁,获取成功之后才可以往下进行,monitorexit指令执行时会释放掉Monitor锁。
对于同步方法,执行时会检查方法的flag中是否有ACC_SYNCHRONIZED
标记,如果有同样会获取对应的Monitor锁,并在方法执行完毕之后释放锁。
Thread中的一些常用方法
sleep方法
sleep方法用于使线程进入睡眠状态,它可以指定睡眠的时间,并且会抛出InterruptedException异常,因为sleep方法会进入阻塞状态,处于阻塞状态的线程如果被中断,会抛出抛出InterruptedException异常。
sleep方法如果持有某个锁之后进入阻塞状态,此时并不会释放锁。
@Override
public void run() {
try {
Thread.currentThread().sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
interrupt方法
interrupt方法可以中断处于阻塞状态的线程,处于阻塞状态的线程被中断时会抛出一个InterruptedException
异常。
public class IntteruptTest {
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run() {
try {
System.out.println("开始sleep");
// 进入睡眠
Thread.currentThread().sleep(10000);
System.out.println("结束sleep");
} catch (InterruptedException e) { // 如果被中断
System.out.println("sleep被中断");
e.printStackTrace();
}
System.out.println("执行完毕");
}
};
// 启动线程
thread.start();
// 中断线程
thread.interrupt();
}
}
输出:
开始sleep
sleep被中断
执行完毕
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at Thread.IntteruptTest$1.run(IntteruptTest.java:25)
interrupted()方法和isInterrupted()方法
相同点
interrupted()方法和isInterrupted()方法都可以用来获取线程的中断状态,中断状态表示线程是否被中断,初始化及未被中断过的时候,中断状态值为false,当线程被中断后,中断状态为true。
不同点
(1)调用方式不一样:
- interrupted()是Thread类中的方法,可以直接通过
Thread.interrupted()
进行调用; - isInterrupted()方法需要获取到当前的线程再继续调用
Thread.currentThread().isInterrupted()
;
(2)interrupted()方法调用的时候会判断中断状态的值,如果已经被中断过(值为true),会将其恢复为false状态。
public class Test {
public static void main(String[] args) {
// 初始状态,当前线程未被中断过,所以返回false
System.out.println("1. " + Thread.currentThread().isInterrupted());
// 当前线程未被中断过,同样返回false
System.out.println("2. " + Thread.interrupted());
// 中断当前线程
Thread.currentThread().interrupt();
// 线程被中断,此时isInterrupted()返回true
System.out.println("3. " + Thread.currentThread().isInterrupted());
// interrupted同样返回true,表示线程被中断过,处于此状态时,它会将中断标识的状态由true置为false,恢复未中断的状态
System.out.println("4. " + Thread.interrupted());
// 由于中断状态被上一步的Thread.interrupted()恢复为了false,所以这里返回false
System.out.println("5. " + Thread.currentThread().isInterrupted());
// 中断状态已经处于false状态,所以同样返回false
System.out.println("6. " + Thread.interrupted());
}
}
输出:
1. false
2. false
3. true
4. true
5. false
6. false
从源码中可以看到 isInterrupted
调用的是navtive的isInterrupted方法来实现的:
public class Thread implements Runnable {
public boolean isInterrupted() {
// 调用原生的isInterrupted
return isInterrupted(false);
}
// native方法
private native boolean isInterrupted(boolean ClearInterrupted);
}
interrupted()
方法,底层也是调用了navtive的isInterrupted
方法,只不过在参数中传入了true,表示恢复中断标识:
public class Thread implements Runnable {
public static boolean interrupted() {
// isInterrupted是native方法
return currentThread().isInterrupted(true);
}
}
join方法
如果调用某个Thread对象的join方法时,会将调用者所处的线程进入阻塞状态,直到Thread对象中的任务运行完毕,再恢复调用者的线程继续往下运行。
来看个例子:
public class Test {
public static void main(String[] args) {
Thread taskOne = new Thread(new TaskOne());
taskOne.start();
System.out.println("Main线程");
}
}
class TaskOne implements Runnable {
@Override
public void run() {
try {
Thread.sleep(3000);
System.out.println("任务一运行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出:
Main线程
任务一运行完毕
从输出结果中可以看到Main线程打印之后,任务一才运行完毕,假如需要等任务一结束之后才执行Main线程,就可以使用到join方法:
public class Test {
public static void main(String[] args) throws InterruptedException {
Thread taskOne = new Thread(new TaskOne());
taskOne.start();
// 当前线程是Main线程,调用taskOne的join方法时,会使Main线程进入阻塞状态,直到taskOne的run方法中的任务执行完毕
taskOne.join();
// taskOne任务结束之后,恢复Main线程的运行
System.out.println("Main线程");
}
}
输出:
任务一运行完毕
Main线程
CountDownLatch和CyclicBarrier的区别
CountDownLatch
CountDownLatch用于某个线程等待其他线程的任务执行完毕之后再执行。
在创建CountDownLatch的时候可以指定值,调用countDown()
方法会使值减1,调用await()
方法会使调用线程进入阻塞状态,直到CountDownLatch的值变为0再继续向下执行。
public class CountDownLatchTest {
public static void main(String[] args) {
// 创建CountDownLatch,可以指定值,这里设为2
CountDownLatch countDownLatch = new CountDownLatch(2);
// 创建任务1
new Thread(){
@Override
public void run() {
try {
System.out.println("任务一正在运行");
Thread.sleep(3000);
// 值减1
countDownLatch.countDown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}.start();
// 创建任务2
new Thread(){
@Override
public void run() {
try {
System.out.println("任务二正在运行");
Thread.sleep(1000);
// 值减1
countDownLatch.countDown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}.start();
try {
System.out.println("等待任务执行完毕");
// await()方法会使线程进入阻塞状态,countDownLatch的值变为0再继续执行
countDownLatch.await();
System.out.println("所有任务执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
输出:
任务一正在运行
等待任务执行完毕
任务二正在运行
所有任务执行完毕
从输出结果中可以看到,任务一执行完毕之后,会等待任务二执行完毕之后,才会执行await()方法后面的代码。
CyclicBarrier
CyclicBarrier用于一组线程,等待组内其他线程进入某个状态时,组内的所有线程再同时往下进行。
CyclicBarrier待所有的线程资源释放后,可以重新使用,这也是被称之为回环的原因。
public class CyclicBarrierTest {
public static void main(String[] args) {
// 创建CyclicBarrier,数量设为2
CyclicBarrier cyclicBarrier = new CyclicBarrier(2);
// 创建任务1
new Thread(){
@Override
public void run() {
try {
System.out.println("任务一正在运行");
Thread.sleep(3000);
// 这里等待其他线程
// 所有线程都进入等待状态后,所有线程才可以进行往下进行
System.out.println("任务一进入等待");
cyclicBarrier.await();
System.out.println("任务一继续执行");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}.start();
// 创建任务2
new Thread(){
@Override
public void run() {
try {
System.out.println("任务二正在运行");
Thread.sleep(1000);
System.out.println("任务二进入等待");
// 这里等待其他线程
cyclicBarrier.await();
System.out.println("任务二继续执行");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}.start();
}
}
输出:
任务一正在运行
任务二正在运行
任务二进入等待
任务一进入等待
任务一继续执行
任务二继续执行
从输出结果中可以看到,任务二先执行到await()方法,此时任务一还在运行中,所以任务二等待任务一,当任务一也执行到await()方法时,再同时执行await()方法后面的代码。
Semaphore信号量
Semaphore可以用来控制访问受保护资源的线程个数。
在创建Semaphore的时候可以指定个数,表示有多少个线程可以获取到锁,acquire()方法用于获取一个锁,release()方法用于释放一个锁,当指定数量的锁获取完毕之后,如果有新的线程调用acquire方法获取锁只能等待其他线程释放。
public class SemaphoreTest {
public static void main(String[] args) {
// 创建Semaphore,值设置为3,表示可以有3个线程获取到锁
Semaphore semaphore = new Semaphore(3);
// 创建5个线程
for(int i=1; i<=5; i++) {
int j = i;
new Thread(){
@Override
public void run() {
try {
// 获取锁
semaphore.acquire();
System.out.println("第"+j+"个线程获取到锁,开始执行");
Thread.sleep(new Random().nextInt(5000));
System.out.println("第"+j+"个线程执行完毕,并释放锁");
// 释放锁
semaphore.release();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}.start();
}
}
}
输出:
第1个线程获取到锁,开始执行
第2个线程获取到锁,开始执行
第3个线程获取到锁,开始执行
第1个线程执行完毕,并释放锁
第4个线程获取到锁,开始执行
第4个线程执行完毕,并释放锁
第5个线程获取到锁,开始执行
第5个线程执行完毕,并释放锁
第3个线程执行完毕,并释放锁
第2个线程执行完毕,并释放锁
由于设置了锁的数量,从输出结果中可以看到,前三个线程获取到锁之后,其他线程只能等待,当线程1执行完毕释放锁之后,线程4才能申请到锁,执行任务。
标签:面试题,Java,Thread,System,线程,println,多线程,public,out From: https://www.cnblogs.com/shanml/p/17594571.html