多线程
进程是一个应用程序,线程是一个进程中的执行场景/执行单元
一个进程可以启动多个线程
java HelloWord 回车后,会先启动一个主线程调用main方法,同时启动垃圾回收线程负责看护回收垃圾;
多个线程 堆内存和方法区内存共享,栈内存独立,一个线程一个栈。
假如启动10个线程,会有10个栈空间,每个栈间互不干扰,各执行各的,这就是多线程并发
- 实现线程的第一种方式
编写一个类直接继承java.lang.Thread,重写run方法
调用对象的start()方法,启动一个分支线程,开辟一个新的栈空间,start()方法随后就结束了。启动成功的线程会自动调用run方法
如果直接调用run方法,还是在同一个线程,不会创建新的线程,不能并发
- 实现线程的第二种方式
编写一个类,实现 java.lang.Runnable 接口,实现run方法
创建一个对象,调用 Thread t = new Thread(对象) 封装一个线程对象,使用 t.start() 启动线程
第二种方式可改造成 匿名内部类
使用第二种方式居多,因为第二种类可以继承其他类,第一种则不可以
- 线程生命周期
1、新建状态:new 线程对象
2、就绪状态:调用start()方法,线程具有抢夺CPU时间片的权利(执行权),抢夺到执行权就执行run方法进入运行状态
3、运行状态:run方法的开始执行进入运行状态;当之前占有的CPU时间片用完后重新回到就绪状态继续抢夺执行权,当再次抢到执行时间片后,会进入run方法继续执行上一次代码
4、阻塞状态:当线程遇到阻塞事件,例如接收用户键盘输入、sleep、join方法等,线程会进入阻塞状态,线程会放弃之前占有的CPU时间片; 当阻塞结束会进入就绪状态抢夺时间片
5、 锁池lockpool(属于阻塞状态):线程遇到synchronized关键字,会让出执行权,到锁池找对象锁,如果没找到会一直找,找到了进入就绪状态
6、JVM会在就绪状态和运行状态之间进行调度
7、死亡状态:run方法执行结束
- 其它内容
Thread t = new Thread();
获取当前线程对象:Thread.currentThread()
获取线程对象名字:t.getName()
线程默认名称 Thread-0/Thread-1/Thread-2…
修改线程对象名字:t.setName()
让当前线程进入阻塞状态:Thread.sleep(毫秒)
终止线程的休眠:t.interrupt() 依靠异常机制,线程中sleep方法会抛出异常,终止睡眠,继续执行后面的方法
强行终止线程的执行:t.stop();已过时,缺点是容易丢数据
常用的终止线程方法:在run方法中打一个成员变量标记flag,主线程的修改flag值,如果为true,则return run方法,来终止线程
线程合并:t.join()方法,等待子线程运行,当前线程进入阻塞状态,不是子线程栈消失,而是两个栈发生了等待关系
- 线程调度模型
抢占式调度模型:线程优先级高,大概率抢到的CPU时间片多一些,java采用该模型
均分式调度模型:平均分配CPU时间片,每个线程执行的时间片长度一样,有些编程语言的线程调度模型采用这种方式
void setPriority(int newPriority) 设置线程优先级
int getPriority() 获取线程优先级
最低优先级1,默认优先级5,最高优先级10
- 线程安全(重点)
多线程并发的情况下,数据的安全问题
服务器是支持多线程的,服务器已经定义了线程,线程对象的创建、启动等,这些代码我们都不用写。而我们编写的程序放到多线程环境下运行,更应该注重的是多线程下数据安全问题
线程不安全:多线程同时间对同一个数据修改,会出现数据不安全问题
解决线程不安全:线程排队执行,称为线程同步机制。会牺牲一部分效率,但是数据安全第一位
线程同步机制:
synchronized(共享对象){
//线程同步代码块
} 范围越小,效率越高
synchronized出现在实例方法上
对象锁默认为this对象,切不能更改为其它对象
缺点:不能选择对象锁,方式不灵活;同步整个方法体,可能无辜扩大同步范围,导致效率低
优点:代码少了,节简了
synchronized 出现在静态方法上
找类锁,类锁只有一把,无论创建多少对象
- synchronized 关键字
每个对象都有把锁,对 共享对象 进行加锁,执行线程同步代码块前都会检查对象是否加锁,如果有锁需要等待
- 死锁
线程1 synchronized 中锁住第一个对象,嵌套 synchronized 锁第二个对象时,发现被其他线程占用需要等待,
线程2 synchronized 中锁住第二个对象,嵌套 synchronized 锁第一个对象需要等待。
上述场景,造成死锁,程序一直等待中,不会出异常,这种最难调试
synchronized最好不要嵌套使用,容易发生死锁
- 解决线程安全问题
1、尽量使用局部变量代替“实例变量和静态变量”
2、如果是实例变量,可以考虑创建多个对象,对象不共享,就不存在数据安全问题
3、如果没办法,就只能使用线程同步机制了
- 用户线程和守护线程
垃圾回收器就是守护线程,可以理解为后台线程
守护线程一般是一个死循环,所有的用户线程结束,守护线程也结束
main方法是一个用户线程
t.start()之前将用户线程设置为守护线程 t.setDaemon(true)
true表示为守护线程,false表示用户线程
- 定时器
间隔一定的时间去执行特定的程序
1、使用sleep()休眠一定的时间(最原始方式)
2、使用 java.util.Timer,开发中很少用,很多高级框架都支持定时任务,例如SpringTask框架,底层也是用Timer
Timer timer = new Timer()
timer.schedule(task)
- 实现线程的第三种方式(JDK8新特性)
java.util.concurrent.FutureTask 方式,实现java.util.concurrent.Callable 接口,可以获取线程返回值
myClass implements Callable 实现call方法,相当于run方法
FutureTask task = new FutureTask(myClass);
Thread t = new Thread(task);
t.start(); 启动线程
Object o = task.get(); 获取返回值,会等待线程的执行,当前线程处于阻塞状态
- Object类中的 wait 和 notify 方法(生产者和消费者模式)
Object中自带这两种方法
wait()作用:
比如Object o = new Object()
o.wait(); 让正在o对象上活动的线程进入等待状态,无限期等待直到被唤醒为止
notify()作用:
o.notify(); 唤醒正在o对象等待的线程
o.notifyAll(); 唤醒o对象处于等待的所有线程
生产者和消费者模式为了专门解决某个特定需求的
一个线程负责生产,一个线程负责消费,生产满了不能再生产,必须等消费线程消费;消费完了就不能消费,必须等生产线程进行生产
这两个方法的使用建立在synchronized线程同步的基础上
o.wait() 会让正在o对象上活动的线程进入等待状态,并且释放之前占有o对象上的锁;o.notify() 只会通知,不会释放之前占有的的o对象锁