Java 多线程基础


1/7/2016 JavaSE Concurrent

几个多线程概念的介绍

线程状态转换

  • 新建(new): 新创建一个线程对象
  • 可运行(runnable): 线程对象创建后, 其他线程(比如main线程)调用了该对象的start()方法. 该状态的线程位于可运行线程池中, 等待被线程调度选中, 获取cpu 的使用权 .
  • 运行(running): 可运行状态(runnable)的线程获得了cpu 时间片(timeslice) , 执行程序代码.
  • 阻塞(block): 阻塞状态是指线程因为某种原因放弃了cpu 使用权, 也即让出了cpu timeslice, 暂时停止运行. 直到线程进入可运行(runnable)状态, 才有机会再次获得cpu timeslice 转到运行(running)状态. 阻塞的情况分三种:
    • 等待阻塞: 运行(running)的线程执行o.wait()方法, JVM会把该线程放入等待队列(waitting queue)中.
    • 同步阻塞: 运行(running)的线程在获取对象的同步锁时, 若该同步锁被别的线程占用, 则JVM会把该线程放入锁池(lock pool)中.
    • 其他阻塞: 运行(running)的线程执行Thread.sleep(long ms)或t.join()方法, 或者发出了I/O请求时, JVM会把该线程置为阻塞状态. 当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时, 线程重新转入可运行(runnable)状态.
  • 死亡(dead): 线程run()、main() 方法执行结束, 或者因异常退出了run()方法, 则该线程结束生命周期. 死亡的线程不可再次复生.

新建线程

Thread thread = new Thread();
thread.start();
1
2

这样就开启了一个线程. 有一点需要注意的是

Thread thread = new Thread();
thread.run();
1
2

直接调用run方法是无法开启一个新线程的. start方法其实是在一个新的操作系统线程上面去调用run方法. 换句话说, 直接调用run方法而不是调用start方法的话, 它并不会开启新的线程, 而是在调用run的当前的线程当中执行你的操作.

Thread thread = new Thread("t1"){
	@Override
	public void run(){
		System.out.println(Thread.currentThread().getName());
	}
};
thread.start();
1
2
3
4
5
6
7

如果调用start, 则输出是t1

Thread thread = new Thread("t1"){
	@Override
	public void run(){
		System.out.println(Thread.currentThread().getName());
	}
};
thread.run();
1
2
3
4
5
6
7

如果是run,则输出main. (直接调用run其实就是一个普通的函数调用而已, 并没有达到多线程的作用) run方法的实现有两种方式

第一种方式, 直接覆盖run方法, 就如刚刚代码中所示, 最方便的用一个匿名类就可以实现.

Thread thread = new Thread("t1")
{
	@Override
	public void run()
	{
		// TODO Auto-generated method stub
		System.out.println(Thread.currentThread().getName());
	}
};
1
2
3
4
5
6
7
8
9

第二种方式

# CreateThread3()实现了Runnable接口. 
Thread t1=new Thread(new CreateThread3());
1
2

终止线程

Thread.stop() 不推荐使用. 它会释放所有monitor

在源码中已经明确说明stop方法被Deprecated, 在Javadoc中也说明了原因. 原因在于stop方法太过"暴力"了, 无论线程执行到哪里, 它将会立即停止掉线程.

当写线程得到锁以后开始写入数据, 写完id = 1, 在准备将name = 1时被stop,释放锁. 读线程获得锁进行读操作, 读到的id为1, 而name还是0, 导致了数据不一致. 最重要的是这种错误不会抛出异常, 将很难被发现.

线程中断

public void Thread.interrupt() // 中断线程 
public boolean Thread.isInterrupted() // 判断是否被中断 
public static boolean Thread.interrupted() // 判断是否被中断, 并清除当前中断状态
1
2
3

Java的中断是一种协作机制. 也就是说调用线程对象的interrupt方法并不一定就中断了正在运行的线程, 它只是要求线程自己在合适的时机中断自己. 每个线程都有一个boolean的中断状态(不一定就是对象的属性, 事实上, 该状态也确实不是Thread的字段), interrupt方法仅仅只是将该状态置为true. 对于非阻塞中的线程, 只是改变了中断状态, 即Thread.isInterrupted()将返回true, 并不会使程序停止;

优雅的终止线程

public void run(){ 
    while(true){ 
        if(Thread.currentThread().isInterrupted()){ 
           System.out.println("Interruted!"); 
           break; 
        } 
        Thread.yield(); 
    } 
}
1
2
3
4
5
6
7
8
9

对于可取消的阻塞状态中的线程, 比如等待在这些函数上的线程, Thread.sleep(), Object.wait(), Thread.join(), 这个线程收到中断信号后, 会抛出InterruptedException, 同时会把中断状态置回为false.

对于取消阻塞状态中的线程:

public void run(){
    while(true){
        if(Thread.currentThread().isInterrupted()){
            System.out.println("Interruted!");
            break;
        }
        try {
           Thread.sleep(2000);
        } catch (InterruptedException e) {
           System.out.println("Interruted When Sleep");
           //设置中断状态, 抛出异常后会清除中断标记位
           Thread.currentThread().interrupt();
        }
        Thread.yield();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

线程挂起

挂起(suspend)和继续执行(resume)线程

  • suspend()不会释放锁
  • 如果加锁发生在resume()之前 , 则死锁发生

这两个方法都是Deprecated方法, 不推荐使用. 原因在于, suspend不释放锁, 因此没有线程可以访问被它锁住的临界区资源, 直到被其他线程resume. 因为无法控制线程运行的先后顺序, 如果其他线程的resume方法先被运行, 那则后运行的suspend, 将一直占有这把锁, 造成死锁发生.

使用代码模拟:

public class Test{
	static Object u = new Object();
	static TestSuspendThread t1 = new TestSuspendThread("t1");
	static TestSuspendThread t2 = new TestSuspendThread("t2");
	public static class TestSuspendThread extends Thread{
		public TestSuspendThread(String name){
			setName(name);
		}
		@Override
		public void run(){
			synchronized (u){
				System.out.println("in " + getName());
				Thread.currentThread().suspend();
			}
		}
	}
	public static void main(String[] args) throws InterruptedException{
		t1.start();
		Thread.sleep(100);
		t2.start();
		t1.resume();
		t2.resume();
		t1.join();
		t2.join();
	}
}
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

让t1,t2同时争夺一把锁, 争夺到的线程suspend, 然后再resume, 按理来说, 应该某个线程争夺后被resume释放了锁, 然后另一个线程争夺掉锁, 再被resume.

in t1
in t2
1
2

说明两个线程都争夺到了锁, 但是控制台的红灯还是亮着的, 说明t1,t2一定有线程没有执行完.

join 和 yeild

yeild是个native静态方法, 这个方法是想把自己占有的cpu时间释放掉, 然后和其他线程一起竞争(注意yeild的线程还是有可能争夺到cpu, 注意与sleep区别). 在javadoc中也说明了, yeild是个基本不会用到的方法, 一般在debug和test中使用.

join方法的意思是等待其他线程结束, 就如suspend那节的代码, 想让主线程等待t1,t2结束以后再结束. 没有结束的话, 主线程就一直阻塞在那里.

public class Test{
	public volatile static int i = 0;
	public static class AddThread extends Thread{
		@Override
		public void run(){
			for (i = 0; i < 10000000; i++)
				;
		}
	}

	public static void main(String[] args) throws InterruptedException{
		AddThread at = new AddThread();
		at.start();
		at.join();
		System.out.println(i);
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

如果把上述代码的at.join去掉, 则主线程会直接运行结束, i的值会很小. 如果有join,打印出的i的值一定是10000000.

join 的本质:

while(isAlive()) { 
   wait(0); 
}
1
2
3

join()方法也可以传递一个时间, 意为有限期地等待, 超过了这个时间就自动唤醒. 这样就有一个问题, 谁来notify这个线程呢, 在thread类中没有地方调用了notify? 在javadoc中, 找到了相关解释. 当一个线程运行完成终止后, 将会调用notifyAll方法去唤醒等待在当前线程实例上的所有线程,这个操作是jvm自己完成的. 所以javadoc中还给了我们一个建议, 不要使用wait和notify/notifyall在线程实例上. 因为jvm会自己调用, 有可能与你调用期望的结果不同.

守护线程

  • 在后台默默地完成一些系统性的服务, 比如垃圾回收线程、JIT线程就可以理解为守护线程.
  • 当一个Java应用内, 所有非守护进程都结束时, Java虚拟机就会自然退出.

开启守护进程:

Thread t=new DaemonT(); 
t.setDaemon(true); 
t.start();
1
2
3

线程优先级

public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
1
2
3

线程优先级只是表示获取锁的概率大小

基本的线程同步操作

synchronized有三种加锁方式:

  • 指定加锁对象: 对给定对象加锁, 进入同步代码前要获得给定对象的锁.
  • 直接作用于实例方法: 相当于对当前实例加锁, 进入同步代码前要获得当前实例的锁.
  • 直接作用于静态方法: 相当于对当前类加锁, 进入同步代码前要获得当前类的锁.

作用于实例方法, 则不要new两个不同的实例 作用于静态方法, 只要类一样就可以了, 因为加的锁是类.class, 可以new两个不同实例.

wait 和 notify 的用法: 用什么锁住, 就用什么调用wait和notify

Last Updated: 7/3/2019, 6:17:56 PM