一、线程的基础知识
1. 多线程使用经典案例:
你有一家餐厅,并且你雇佣了一名服务员(相当于一条单线程)。当你的餐厅的食客数量不多的时候,一名服务员可以按顺序去做,招呼客人,点菜,上菜,清理桌面等工作。
但是当客人非常多时,你发现一名服务员不够用了,这是你会在招几名服务员(新开了几条线程),来更好的服务客人。
2. 进程
通常运行中的服务对应一个进程,就像上述提供餐饮的这个服务,可以比喻成一个进程,在这里进程里,所有的服务员共享餐厅里的物品,比如服务员都可以收拾桌子,都可以上菜。(所有线程共享进程内的全部资源),但是如果有2家餐厅,那么A餐厅肯定不能随便去B餐厅拿桌子的,除非B餐厅允许A餐厅去拿东西。(在没有进程允许下,不允许访问该进程的资源)
3.并发性 与 并行性
同一时刻只能有一条指令被执行,因为cpu处理速度非常快,不同的指令会被快速轮换,造成宏观并行的效果。(并发性)
同一时刻有多条指令被执行。(并行性)
4、关于多线程的并发问题
可以想象一下,服务员A辛辛苦苦的把盘子洗了一半,然后老板叫A去上菜了,接着服务员B不知道盘子已经洗过一半了,又去把前一半洗了一遍。。造成急需盘子的时候,B还在洗盘子,导致客人都跑光了。。。B也被解雇了 :(
二、多线程的创建
1. 继承自Thread类
public class MyThread extends Thread{
private int i ;
public MyThread(String name){
super(name);
}
//run 方法称为线程执行体
public void run() {
for(;i<20;i++){
System.out.println(Thread.currentThread().getName()+"-"+i);
}
}
public static void main(String[] args){
System.out.println(Thread.currentThread().getName()+"-主线程");
new MyThread("新线程A").start();
new MyThread("新线程B").start();
}
}
2. 实现Runnable接口
public class MySecondThread implements Runnable{
private int i ;
@Override
public void run() {
// TODO Auto-generated method stub
for(;i<10;i++){
System.out.println(Thread.currentThread().getName()+"-"+i);
}
}
public static void main(String[] args){
System.out.println(Thread.currentThread().getName()+"-主线程");
MySecondThread mt = new MySecondThread();
Thread a = new Thread(mt);
Thread b = new Thread(mt);
a.setName("线程A");
b.setName("线程B");
a.start();
b.start();
}
}
这两者区别在: 数据同步上,继承方法实现的Thread并没有实现数据共享,所以,两个线程需要执行40次.而实现Runnable接口实现了数据同步,仅仅2个线程体,执行同一个操作的时候,只需要执行10次。
三、线程的生命周期
1. 线程的生命周期有5个状态:
新建 --> 就绪 --> 运行 --> 阻塞 --> 死亡
1.1 当一个Thread对象被创建时, 线程处于 新建状态,类似于你买下了一块地皮,准备开餐厅。
1.2 当Thread调用start()方法后, 线程处于就绪状态,类似于你的店装修完毕,开张了等待客人状态。
1.3 运行 ,是等待JVM里的程序调度器的调度 ,就像客人来与不来,我们控制不了,这得看客人心情的。
1.4 阻塞 ,当其他线程轮换后,会阻塞当前线程 。 就像,我们在餐厅,排队打饭菜,排在前面的人A刚好接了个电话,然后无法同时打饭菜,后面的B看到了就上前先打饭菜了,这时A就被阻塞了。
2.不要对线程调用2次以上的start()方法,已经死亡的线程无法使用start()让他重新被调用,否则会抛出IllegalThreadStateException异常。
3.运行 与 阻塞状态的 切换
3.1 线程调用sleep()方法主动放弃所占用的系统资源,因为cpu是不会偷懒的,它会在当前线程sleep时,主动唤醒运行其他就绪的线程。
3.2 线程主动调用一个阻塞式的IO方法,在方法返回之前,该线程将被阻塞。
3.3 该线程试图获得一个同步监视器,但是该同步监视器被其他线程所持有。
3.4 线程等待某个通知(notify)。
3.5 线程调用suspend方法将线程挂起。该方法容易导致死锁,尽量避免使用该犯法。
4. 线程死亡
4.1 run方法体执行完成
4.2 线程抛出异常
4.3 使用stop()方法结束该进程。但是该方法容易导致死锁。
可以使用isAlive()方法判断该线程是否 死亡 或 新建 return false;其他状态 return true;
四、控制线程
1. Join () 该方法需 throws InterruptedException
MyThread join = new MyThread("Join线程");
join.start();
join.join();
这时,将会阻塞main线程,直到join线程被执行完后,其他线程才有被执行的机会。
几个比较常用的方法:
sleep() 线程休眠,
yield() 线程让步,
setPriority();设置线程优先级
五、线程的同步
线程间最重要的问题就是线程安全问题,因为正在执行的线程随时可能被停止轮换到另外一个线程,
这时候线程间对,共享数据的读写将操作互斥,造成读取 “脏数据” 。
典型的例子,取钱问题。你的账户有1000,你与你朋友同时去取,每人取800。
这个问题的逻辑很简单
if( 余额 > 800){
输出余额;
/***** 我是分割线 ****/
计算新余额 = 余额 - 800;
}
因为有两个人并发去操作假如A与B,当A运行到 【分割线】位置,该线程被JVM调度器阻塞掉,让B去执行。(因为线程随时可能被阻塞),此时B读取的余额数 仍然是1000,
B执行的很顺畅,直接执行完毕之后,现在的余额其实是200,但是因为A被阻塞的位置在if()语句块内,所以在解除阻塞后,会接着计算新的余额,也就是 -600。1000块钱的卡,
能取出1600么?还记得前几个月,有人因为取款机多取出钱,被枪毙的事情吗?惊悚啊。。这时就用到了线程同步。
使用synchronizated(obj){}同步代码块,监视obj对象,同时只允许一个线程操作obj对象。当一个线程操作obj时,对obj加锁,结束操作后对obj解锁。
synchronizated 也可以修饰方法
public synchronizated void function(){}
同步锁 Lock ,锁的类别有
代码的格式
private final ReentrantLock lock = new ReentrantLock();
@SuppressWarnings("unused")
private void EnsureSavetyThreadMethod(){
lock.lock();
try {
// 需要保证线程安全的代码
} catch (Exception e) {
// TODO: handle exception
} finally {
//保证结束后释放锁
lock.unlock();
}
}
死锁
死锁发生的原因在于,两个线程互相等待对象释放加锁的资源,造成的僵持。就像两个人,一个进门一个出门,互相礼让,最后谁也没能走出这扇门。