Java 多线程基础
几个多线程概念的介绍
线程状态转换
- 新建(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();
2
这样就开启了一个线程. 有一点需要注意的是
Thread thread = new Thread();
thread.run();
2
直接调用run方法是无法开启一个新线程的. start方法其实是在一个新的操作系统线程上面去调用run方法. 换句话说, 直接调用run方法而不是调用start方法的话, 它并不会开启新的线程, 而是在调用run的当前的线程当中执行你的操作.
Thread thread = new Thread("t1"){
@Override
public void run(){
System.out.println(Thread.currentThread().getName());
}
};
thread.start();
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();
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());
}
};
2
3
4
5
6
7
8
9
第二种方式
# CreateThread3()实现了Runnable接口.
Thread t1=new Thread(new CreateThread3());
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() // 判断是否被中断, 并清除当前中断状态
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();
}
}
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();
}
}
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();
}
}
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
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);
}
}
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);
}
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();
2
3
线程优先级
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
2
3
线程优先级只是表示获取锁的概率大小
基本的线程同步操作
synchronized有三种加锁方式:
- 指定加锁对象: 对给定对象加锁, 进入同步代码前要获得给定对象的锁.
- 直接作用于实例方法: 相当于对当前实例加锁, 进入同步代码前要获得当前实例的锁.
- 直接作用于静态方法: 相当于对当前类加锁, 进入同步代码前要获得当前类的锁.
作用于实例方法, 则不要new两个不同的实例 作用于静态方法, 只要类一样就可以了, 因为加的锁是类.class, 可以new两个不同实例.
wait 和 notify 的用法: 用什么锁住, 就用什么调用wait和notify