一、命名与获取
多线程每一次运行都是不同的运行结果,因为它会根据自己的情况进行资源抢占(除非加锁)。要区分每一个进程,那么必须依靠线程的名字。对于线程名字,一般而言会在其启动之前进行定义,不建议对已经启动的线程更改名称/设置不同线程重名。
如果要进行线程名称的操作,可以使用Threa类如下方法:
构造方法:
/**
* Allocates a new {@code Thread} object. This constructor has the same
* effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}
* {@code (null, target, name)}.
*
* @param target
* the object whose {@code run} method is invoked when this thread
* is started. If {@code null}, this thread's run method is invoked.
*
* @param name
* the name of the new thread
*/
public Thread(Runnable target, String name) {
init(null, target, name, 0);
}
普通方法:
/**
* Changes the name of this thread to be equal to the argument
* <code>name</code>.
* <p>
* First the <code>checkAccess</code> method of this thread is called
* with no arguments. This may result in throwing a
* <code>SecurityException</code>.
*
* @param name the new name for this thread.
* @exception SecurityException if the current thread cannot modify this
* thread.
* @see #getName
* @see #checkAccess()
*/
public final synchronized void setName(String name) {
checkAccess();
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
if (threadStatus != 0) {
setNativeName(name);
}
}
与之对应我们可以用getName()方法取得名字
对于线程名字操作会出现一个问题,这些方法属于Thread类,Runnable接口子类无法取得线程名字,能取得的只有当前正在执行的线程名。
二、start&&run
2.1 start
使线程变为就绪状态,Java虚拟机会开始调用这个线程的run方法。调用后会出现两个并发的进程,一个是调用start方法的进程(main进程),另一个就是执行run方法的,被刚刚启动的进程。
注意:
对于一个线程多次调用start是非法的。尤其是当一个线程已经执行完毕后,不能通过再次调用start来重启这个线程。
/**
* @exception IllegalThreadStateException if the thread was already
* started.
* @see #run()
* @see #stop()
*/
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*/
//threadStatus为0代表状态为“NEW”,如果不为0抛出异常
if (threadStatus != 0)
throw new IllegalThreadStateException();
//将当前线程加入到线程组中
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
使用start方法才真正实现了多线程运行。使用了start方法后我们的线程没有立即的执行,而是得等待,等到我们的cpu有空闲的时候,才会执行线程里面的run方法。等run方法执行完,线程就结束了。
2.2 run
如果这个Thread是由Runnable对象构造而来的,那么Runnable对象的run方法会被调用,否则这个方法什么都不做并返回。
Thread类的子类应该重写这个方法。
/**
* @see #start()
* @see #stop()
* @see #Thread(ThreadGroup, Runnable, String)
*/
@Override
public void run() {
if (target != null) {
target.run();
}
}
其中target如下:
/* What will be run. */
private Runnable target;
可以看出target就是Runnable对象本身。
直接使用Thread执行run方法,这个时候只会运行在我们的主线程中,就和普通的函数调用一样。执行顺序肯定是顺序执行,没有达到多线程的目的。
2.3 run和start对比
下面通过具体的代码例子来说明这一点
class MyThread extends Thread{
private String name;
public MyThread(String name){
this.name=name;
}
@Override
public void run(){
for(int i=0;i<200;i++){
System.out.println(this.name+"--->"+i);
}
}
}
public class test{
public static void main(String[] args) {
MyThread thread1=new MyThread("test1");
MyThread thread2=new MyThread("test2");
MyThread thread3=new MyThread("test3");
thread1.run();
thread2.run();
thread3.run();
}
}
程序的输出结果如下:
上面的线程类的功能是实现一个循环输出操作,我们的线程和进程一样,都必须轮流抢占资源。多线程的执行应该是多个线程并发执行。而上面代码得到的结果是顺序执行的结果,显然与初衷不符。因此没有真正启动多线程(仅仅相当于main线程调用了test实例中的方法,而不是多线程),真正启动多线程的是Thread类里面的start()方法。
将上面程序的run修改为start后:
class MyThread extends Thread{
private String name;
public MyThread(String name){
this.name=name;
}
@Override
public void run(){
for(int i=0;i<200;i++){
System.out.println(this.name+"--->"+i);
}
}
}
public class test{
public static void main(String[] args) {
MyThread thread1=new MyThread("test1");
MyThread thread2=new MyThread("test2");
MyThread thread3=new MyThread("test3");
thread1.start();;
thread2.start();
thread3.start();
}
}
输出结果:
三、interrupt&&interrupted&&isInterrupted
Thread类中,与线程中断有关的有三种方法:interrupt()方法、interrupted()方法、isInterrupted()方法,下面对这三种方式进行说明:
3.1 interrupt
一个线程不应该由其他线程来强制中断或停止,而是应该由线程自行停止。所以,thread.stop, Thread.suspend, Thread resume都已经被废弃了。
而Thread.interrupt的作用其实也不是中断线程,而是通知“线程应该中断了”,具体到底中断还是继续运行,应该由被通知的线程自己处理。
具体来说,当对一个线程,调用interrupt()时:
- 如果线程处于被阻塞状态(例如sleep,wait,join等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而已。被设置中断标志的程序将继续正常运行,不受影响。
所以interrupt()并不能真正的中断线程,需要被调用的线程自己配合才行,也就是说,一个线程如果有被中断的需求,那么就可以这样做。
源码如下:
/**
* Interrupts this thread.
*
* <p> Unless the current thread is interrupting itself, which is
* always permitted, the {@link #checkAccess() checkAccess} method
* of this thread is invoked, which may cause a {@link
* SecurityException} to be thrown.
*
* <p> If this thread is blocked in an invocation of the {@link
* Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link
* Object#wait(long, int) wait(long, int)} methods of the {@link Object}
* class, or of the {@link #join()}, {@link #join(long)}, {@link
* #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)},
* methods of this class, then its interrupt status will be cleared and it
* will receive an {@link InterruptedException}.
*
* <p> If this thread is blocked in an I/O operation upon an {@link
* java.nio.channels.InterruptibleChannel InterruptibleChannel}
* then the channel will be closed, the thread's interrupt
* status will be set, and the thread will receive a {@link
* java.nio.channels.ClosedByInterruptException}.
*
* <p> If this thread is blocked in a {@link java.nio.channels.Selector}
* then the thread's interrupt status will be set and it will return
* immediately from the selection operation, possibly with a non-zero
* value, just as if the selector's {@link
* java.nio.channels.Selector#wakeup wakeup} method were invoked.
*
* <p> If none of the previous conditions hold then this thread's interrupt
* status will be set. </p>
*
* <p> Interrupting a thread that is not alive need not have any effect.
*
* @throws SecurityException
* if the current thread cannot modify this thread
*
* @revised 6.0
* @spec JSR-51
*/
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(this);
return;
}
}
interrupt0();
}
3.2 interrupted()
检查当前线程是否已经被中断过,如果已经中断过,那么调用这个方法后中断状态会被清理(连续调用两次这个方法,第一次返回true那么第二次就会返回false,除非这个线程又被中断了)
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
其作用是测试当前线程是否被中断(检查中断标志),返回一个boolean并清除中断状态,第二次再调用时中断状态已经被清除(除非当前线程在第一次调用已清除其中断状态之后且在第二次调用检查之前再次被中断),返回一个false。
只能通过Thread.interrupted()调用
3.3 isInterrupted
检测本线程是否被中断,调用这个方法不会影响这个线程的中断状态
public boolean isInterrupted() {
return isInterrupted(false);
}
这个方法调用了native方法
/**
* 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);
3.4 区别
3.4.1 interrupted和isInterrupted的区别
从源代码可以看出,这两个方法都是调用的isInterrupted(boolean ClearInterrupted),只不过interrupted带的参数是true,isInterrupted带的参数是false。
因此第一个区别就是,一个会清除中断标志位,另一个不会清除中断标志位。
再分析源码,可以看出第二个区别在return上:
interrupted测试的是当前线程的中断状态。而isInterrupted测试的是调用改方法的对象所表示的线程。一个是静态方法,测试的是当前线程,另一个是实例方法,测试的是实例对象所表示的线程的中断状态。
下面用一个具体的例子来进一步阐述这个区别:
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class test {
public static void main(String[] args) {
try{
MyThread thread=new MyThread();
thread.start();
Thread.sleep(1000);
thread.interrupt();
System.out.println("第一次测试是否停止:"+thread.interrupted());
//这里会输出false
System.out.println("第二次测试是否停止:"+thread.interrupted());
//这里同样会输出false,因为main线程没有被中断
}catch (Exception e){
}
}
}
class MyThread extends Thread{
@Override
public void run(){
for(int i=0;i<500000;i++){
System.out.println("i="+i);
}
}
}
在第五行启动thread线程,第6行使main线程睡眠1秒钟从而使得thread线程有机会获得CPU执行。
main线程睡眠1s后,恢复执行到第7行,请求中断thread线程。
第9行测试线程是否处于中断状态,注意,这里测试的是main线程,因为:
- interrupted测试的是当前线程的中断状态
- main线程执行了第9行语句,所以main线程是当前线程
再看isInterrupted方法的实例:
public class test {
public static void main(String[] args) {
try{
MyThread thread=new MyThread();
thread.start();
Thread.sleep(1000);
thread.interrupt();
System.out.println("是否停止:"+thread.isInterrupted());
}catch (Exception e){
}
}
}
这里会输出true,因为调用的是isInterrupted方法,因此测试的是thread对象所代表的线程的中断状态。
四、wait和sleep
4.1 wait和sleep之间的一般差异
简单地说,wait是一个用于线程同步的实例方法。
它可以在任何对象上调用,因为它在java.lang.Object上定义,但它只能从synchronized块中调用。它释放对象的锁定,以便另一个线程可以跳入并获取锁。当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
另一方面,Thread.sleep()是一个可以从任何上下文调用的静态方法。Thread.sleep()暂停当前线程并且不释放任何锁。sleep()方法导致了程序暂停执行指定的时间,让出CPU给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.*;
public class test {
public static void main(String[] args) throws Exception {
test t=new test();
test.sleepWaitExamples();
}
private static Object LOCK = new Object();
private static void sleepWaitExamples()
throws InterruptedException {
Thread.sleep(1000);
System.out.println(
"Thread '" + Thread.currentThread().getName() +
"' is woken after sleeping for 1 second");
synchronized (LOCK) {
LOCK.wait(1000);
System.out.println("Object '" + LOCK + "' is woken after" +
" waiting for 1 second");
}
}
}
输出:
Thread 'main' is woken after sleeping for 1 second
Object 'java.lang.Object@1b6d3586' is woken after waiting for 1 second
4.2 结论
通常,我们应该使用sleep()来控制一个线程的执行时间,使用wait()来进行多线程同步。
五、notify和notifyAll
5.1 概念补充:锁池和等待池
- 锁池: 假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
- 等待池: 假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中。
5.2 区别
- 如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
- 当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
- 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
综上,所谓唤醒线程,另一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁释放后再次参与竞争。而notify只会唤醒一个线程。
5.3 为什么wait和notify必须在同步代码块中调用
由于wait()属于Object方法,调用之后会强制释放当前对象锁,所以在wait() 调用时必须拿到当前对象的监视器monitor对象。因此,wait()方法在同步方法/代码块中调用。
参考:
参考
参考
notify和notifyAll
锁池和等待池
线程间协作:wait、notify、notifyAll