近日面试经常被问起关于java多线程的相关问题,觉得需要总结和学习一下。(后续还会有所补充)
一.一些常用的定义
进程:是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源
线程:在同一个进程内有可以执行多个任务,而这每一个任务可看作是一个线程,是程序的执行单元,执行路径。各个线程之间共享程序的内存空间(代码段,数据段,堆空间)及一些进程级的资源,但是各个线程拥有自己的栈空间。
多进程是为了提高CPU使用率
多线程是指程序有多条执行路径,提高应用程序的使用率
执行多线程的优点:减少程序的响应时间
与进程相比,线程的创建和切换开销更小
使用多线程能简化程序结构,使程序便于理解和维护。
线程的四种状态:运行,就绪,挂起,结束
并行和并发:前者是逻辑上同时发生,指在某一个时间内同时运行多个程序
后者是物理上同时发生,指在某一个时间点同时运行多个程序
Java程序运行的原理:由Java命令启动JVM,JVM启动就相当于启动了一个进程。
接着由该进程创建一个主线程去调用main方法,jvm虚拟机的启动是多线程的。
二.实现java多线程的三种方式
1.继承Thread类
继承Thread类之后需要重写其中的run方法,run方法的内部就是多线程的逻辑处理。Thread本质上也是实现了Runnable接口的一个实例,并且启动线程的唯一方式就是调用Thread类的start()方法。start方法是一个本地方法,他将启动一个新的线程,并执行run()方法。
调用start方法后并不是立即执行多线程代码,而是使得该线程变成可运行态(Runnable)
2.实现Runnable接口,并实现该接口的run()方法
实例化自定义类,并将实例化对象通过 Thread t= new Thread(自定义的对象)的构造方法传入。
而后调用Thread类的start方法,启动多线程。
3.实现Callable接口,重写call方法
callable接口实际是属于Executor框架中的功能类
Callable接口可以提供一个返回值,Runnable无法提供
Callable中的Call()方法可以抛出异常,而Runnable的run()方法不能抛出异常
Callable可以拿到一个Future对象,通过future对象可以监视目标线程调用call()方法的情况。当Future调用get方法时,当前线程就会阻塞,直到call方法结束返回结果。
ExecutorService threadPool = Executors.newSingleThreadExecutor();
Future<String> future = threadPool.submit(new CallableTest());
future.get()
4.关于这三种方法的比较
一般情况推荐使用实现Runnable接口的方法,因为实现接口可以避免Java只可以进行单继承的局限性
同时也符合代码的开发习惯,一般只有一个类需要被加强或者修改的时候才会采用继承策略,而如果我们继承Thread类去实现多线程时,我们只是重写了其中的run方法。同时实现Runnable接口也适合多个相同程序代码去处理 同一资源的情况,把线程同程序的代码,数据有效的分离。
注意:关于Runnable接口对于资源共享的一些想法
当我们在实现多线程时分别考虑继承Thread类和Runnable接口,在初始化时操作是不一样的,当我们在继承Thread类之后,比如新建MyThread类;在主程序中调用start之前必然会创建Mythread对象,而当我们创建多个线程时,自然会new出来多个对象,而这多个对象有相互独立run该方法,所以Thread是无法进行资源共享的。而当我们通过实现Runnable接口时,我们创建自定义对象实例是单一的,在创建多线程对象时将这个单一的实例传入到构造方法中。而封装在自定义的单一实例里面的数据就可以进行共享。这就是这两者的区别。
在一些博客中发现有人通过和Runnable接口相同的方式去创建Thread类的多线程对象,这在理论上是可以的,因为Thread类本身就是一个Runnable接口的实例,但是当接口被实例化之后,我们再去使用实例化之前接口中的方法是多此一举的行为,并且实不符而常理的。
附上两种方式创建多线程的代码:
自定义MyThread类继承Thread类
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();t2.start();t3.start();
自定义MyThread类 实现Runnable接口
MyThread thread = new MyThread();
Thread t1 = new Thread(thread);
Thread t2 = new Thread(thread);
Thread t3 = new Thread(thread);
t1.start();t2.start();t3.start();
另附上一个博客中对于数据共享的一点总结,我感觉此处的理解十分深刻。
如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,有如下两种方式来实现这些Runnable对象之间的数据共享:
1、将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个Runnable对象。每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行的各个操作的互斥和通信。
2、将这些Runnable对象作为某一个类中的内部类,共享数据作为这个外部类中的成员变量,每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行的各个操作的互斥和通信,作为内部类的各个Runnable对象调用外部类的这些方法。
3、上面两种方式的组合:将共享数据封装在另外一个对象中,每个线程对共享数据的操作方法也分配到那个对象身上去完成,对象作为这个外部类中的成员变量或方法中的局部变量,每个线程的Runnable对象作为外部类中的成员内部类或局部内部类。
三.线程的同步
Java提供了两个关键字来实现同步(synchronized 和 lock)
1.synchronized关键字
①.在方法声明前加入synchronized的关键字
public synchronized void mutiThreadAccess()
将需要被同步的资源的操作放到mutiThreadAccess()方法中,就能保证这个方法在同一时刻只能被一个线程访问,如果在声明的时候方法体特别大,则程序的执行效率会受到影响。
注意:在定义接口方法时不能使用synchronized关键字。
构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
synchronized 关键字不能被继承 。如果子类覆盖了父类的 被 synchronized 关键字修饰的方法,那么子类的该方法只要没有 synchronized 关键字,那么就默认没有同步,也就是说,不能继承父类的 synchronized。
②.修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象
class ClassName {
public void method() {
synchronized(ClassName.class) {
// todo
}
}
}
③.synchronized块
当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
以上规则对其它对象锁同样适用.
④.一些总结
修饰一个类:其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象;
修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
修饰一个静态的方法:其作用的范围是整个方法,作用的对象是这个类的所有对象;
修饰一个代码块:被修饰的代码块称为同步语句块,其作用范围是大括号{}括起来的代码块,作用的对象是调用这个代码块的对象;
如果两个线程访问同一个对象中的synchronized(this)同步代码块,第二个访问的线程会被阻塞,如果两个线程访问的是两个对象中的代码块,则不受影响。
其实无论synchronized关键字加在方法还是对象上,如果它作用的对象是非静态的,则取得得锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是类,该类所有对象同一把锁。每个对象只有一个锁与之相关联,谁能拿到这个锁谁就能执行这段代码。
2.lock关键字
在jdk1.5之后,新增了Lock接口来实现线程的同步,其中我们使用ReentrantLock来进行实例化
Lock lock = new ReentrantLock();
其中Lock提供了几种方法来进行同步操作:
(1)lock:以阻塞的方式来获取锁,即如果获取到锁则立即返回,如果获取不到,则一直等到其他线程释放锁,获取到锁之后返回。
(2)tryLock:以非阻塞的方式获取锁,获取到锁则返回true,否则立即返回false
(3)tryLock(long timeout, TimeUnit unit)获取到锁则返回true,若获取不到则等待参数给定的时间单元,在给定时间内获取到锁则返回true,获取不到则返回false
(4)lockInterruptibly():如果获取到锁,立即返回;如果没有获取到锁,当前线程处于休眠状态,直到获得锁,或者被其他线程中断(会收到InterruptedException异常)。这个方法与lock()方法最大的区别在于如果lock()方法获取不到锁,会一直处于阻塞状态,而会忽略interrupt()引起的异常。
注意:关于interrupt()方法的一些总结
java中的中断机制是围绕interrupt status(中断状态)这个字段来工作的,在java中代表中断状态的属性是 private volatile Interruptible blocker,关于blocker变量有以下几种操作:
1.默认blocker=null; ®1
2.调用方法“interrupt0();”将会导致“该线程的中断状态将被设置(JDK文档中术语)”。®2
3.再次调用“interrupt0();”将会导致“其中断状态将被清除(同JDK文档中术语)”®3
其中关于中断方法有以下几种:
1.public void interrupt():中断线程。如果线程在调用 Object 类的 wait()、wait(long) 或 wait(long, int) 方法,或者该类的join()、join(long)、join(long, int)、sleep(long) 或 sleep(long, int) 方法过程中受阻,则其中断状态将被清除,它还将收到一个 InterruptedException。
2.public static boolean interrupted(); 测试当前线程是否已经中断。线程的中断状态 由该方法清除。如果该线程已经中断,则返回 true;否则返回 false。
3.public boolean isInterrupted(); 测试线程是否已经中断。线程的中断状态 不受该方法的影响。如果该线程已经中断,则返回 true;否则返回 false。
有关 "interrupted()"和"isInterrupted()"两个方法的相同点和不同点的一些解释:
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
return isInterrupted(false);
}
/**
* Tests if some Thread has been interrupted. The interrupted state
* is reset or not based on the value of ClearInterrupted that is
* passed.
*/
private native boolean isInterrupted(boolean ClearInterrupted);
相同点都是判断线程的interrupt status是否被设置,若被设置返回true,否则返回false.区别有两点:一:前者是static方法,调用者是current thread,而后者是普通方法,调用者是this current.二:它们其实都调用了Java中的一个native方法isInterrupted(boolean ClearInterrupted); 不同的是前者传入了参数true,后者传入了false.意义就是:前者将清除线程的interrupt state(®3),调用后者线程的interrupt state不受影响。
注意: 1.调用interrupt()方法并不会中断一个正在运行的线程.2.若调用sleep()而使线程处于阻塞状态,这时调用interrupt()方法,会抛出InterruptedException,从而使线程提前结束阻塞状态,退出阻塞代码
以下是关于interrupt的源码
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt();
return;
}
}
interrupt0();
}
线程的blocker字段(也就是interrupt status)默认是null。调用interrupt()方法时,只是运行了®2,并没有进入if语句,所以没调用真正执行中断的代码b.interrupt().
3.关于synchronized和lock之间的比较
java提供的两种关键字都可以实现共享资源的同步,synchronized使用Object对象本身的notify,wait,notifyall方法来完成相关的操作,而lock使用Condition进行线程之间的调度。
其中关于Condition的介绍:Condition为一个接口,基本方法是await()和signal()方法。
针对Object的wait方法
void await() throws InterruptedException:当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常;
针对Object的notify/notifyAll方法
void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。
void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程
Condition依赖于Lock方法,声明方式是lock.newCondition。调用condition方法必须在lock的保护之中,在lock.lock()和lock.unlock()之间可以使用
两者的不同具体体现在以下几个方面:
(1)synchronized是托管给JVM控制的,当获取多个锁的时候必须要以相反的顺序释放,是自动解锁的。而Lock需要显示的指定起始位置和终止位置,释放锁必须在finally中进行。
(2)在竞争资源不是很激烈的情况下,synchronized的性能要优于reentrantLock,当竞争激烈的情况下,synchronized的性能会下降的很快。
四.sleep和wait方法以及终止线程的方法
1.sleep和wait方法的区别
(1)原理不同,sleep是Thread类的静态方法,是线程用来控制自身流程的,在sleep方法中设置的时间就像是闹钟,每到固定的时间就自动苏醒。而wait()方法时Object类的方法,用于线程间的通信,线程间的通信是指不同种类的线程对同一资源的操作。
(2)sleep()方法不涉及线程间的通信,因此sleep()不会释放锁,wait()方法会释放锁
(3)wait方法必须放在同步控制方法或者同步语句块中使用,sleep()方法可以放在任何地方使用。但是sleep()方法必须捕获异常,而wait方法以及与其配套的notify(),notifyAll()不需要捕获异常。
2.终止线程的方法
在java中终止线程的方法有stop()和suspend()方法。
当调用stop方法时,他会释放掉已经锁定的所有资源,如果当前任何一个受这些监视资源保护的对象处于一个不一致的状态,则其他线程将会看到这个不一致的状态,可能会导致程序执行的不确定性,并且这种问题很难被定位。
当调用suspend()方法时容易发生死锁,因为suspend()方法不会释放锁,这就导致如果用一个suspend挂起一个有锁的线程,那么在锁恢复之前不会被释放,如果调用suspend方法,线程将试图取得相同的锁,则会发生死锁。
这两种方法都不建议使用,常用的方法是设置一个flag标志来使得线程离开run()方法从而终止线程。同时也可以在run方法中捕获interruptException异常,使得线程安全退出。
五.多线程中的一些其他方法
1.yield()方法和操作系统相关,调用时暂停当前正在执行的线程对象,告诉虚拟机它乐意让其他线程占领自己的位置。
sleep()和yield()方法的相关比较:
(1)sleep()方法再给其他线程运行机会是不考虑线程的优先级,因此会给低优先级的线程机会,而yield()方法只会给相同优先级或者给更高优先级的线程以运行的机会。
(2)线程执行sleep()方法会转入阻塞状态,所以执行sleep()方法的线程在指定时间内坑定不会被执行,而yield()方法只是使当前线程重新回到可执行状态,所以执行yield()方法的线程有可能在进入可执行状态之后马上又被执行。
(3)sleep()方法声明会抛出InterruptException,而yield方法没有申明任何异常。
(4)sleep()方法比yield方法具有更好的可移植性。
2.join()方法是指等待线程终止,举例:在B中调用了线程A的join方法,直到线程A执行完毕,才会继续执行B线程。也可以在join方法中带入参数例如join(2000),代表最多只会等待2s的时间。
3.守护线程
java中的垃圾回收就是典型的守护线程,守护线程又称后台线程。如果所有的用户线程都已经推出,只剩下守护线程,则JVM就会退出。守护线程的优先级一般较低,设置守护线程的方法就是在调用start()方法启动线程之前调用对象的public static void setDaemon(true)方法,如果参数为false则表示是用户进程。
注意:当一个守护线程中产生了其他线程,那么这些新产生的线程默认还是守护线程。
(未完待续)
参考博客:https://www.cnblogs.com/carmanloneliness/p/3516405.html