41. 多线程
主线程:执行主(main)方法的程序
JVM执行main方法,main方法会进入到栈内存,JVM会找操作系统开辟一条main方法通向cpu的执行路径,cpu就可以通过这个路径来执行main方法,而这个路径有一个名字,叫做main(主)线程
创建多线程的第一种方式:==创建Thread的子类==
java.lang.Thread类:是描述线程的类,我们想要实现多线程程序,就必须继承Thread类
实现步骤:
- 创建一个Thread类的子类
- 在Thread类中的子类中重写Thread类中的run方法,设置线程任务(开启任务要做什么?)
- 创建Thread类的子类对象
- 调用Thread类中的start方法,开启新的线程,执行run方法
void start():使该线程开始执行,Java虚拟机调用该线程run方法
结果是两个线程并发的运行,当前线程(从调用返回给start方法)和另一个线程(执行其run方法)
多次启动一个线程是非法的.特别是当线程已经结束执行后,不能再重新启动
==Java程序属于抢占式调度,哪个线程优先级高,哪个线程优先执行;同一个优先级,随机选择一个执行==
获取线程的名称:
- 使用Thread类中的方法getName()
String getName()
- 可以先获取到当前正在执行的线程,使用线程中的方法getName()获取线程的名称
static Thread currentThread()返回对当前正在执行的线程对象的引用
设置线程名称:
- 使用Thread类中的方法setName()
- 使用一个带参数的构造方法,参数传递线程的名称,调用父类的构造方法,把线程名称传递给父类,让父类(Thread)给子线程起一个名字
public static void sleep(long mills)
使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)
创建多线程程序的第二种方式:==实现Runnable接口==
java.lang.Runnable:Runnable接口应该由那些打算通过某一线程执行其实例的类来实现.类必须定义一个成为run的无参数方法
java.lang.Thread类的构造方法
Thread(Runnable target)分配新的Thread对象
Thread(Runnable target, String name)分配新的Thread对象
实现步骤:
- 创建一个Runnable接口的实现类
- 在实现类中重写Runnable接口中的run方法,设置线程等
- 创建一个Runnable接口的实现类方法
- 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
- 调用Thread类中的start方法,开启新的线程run方法
实现Runnable接口创建多线程的好处:
- 避免了单继承的局限性:一个类只能继承一个类,其继承了Thread类就不能继承其他类,实现Runnable接口,还可以继承其他类,实现其它接口
- 增强了程序的拓展性,降低了程序的耦合性:实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦),实现类中,重写了run方法用来设置线程任务.创建Thread类对象,调用start方法用来开启新线程
使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。
使用匿名内部类的方式实现Runnable接口,重新Runnable接口中的run方法:
public class NoNameInnerClassThread {
public static void main(String[] args) {
// new Runnable(){
// public void run(){
// for (int i = 0; i < 20; i++) {
// System.out.println("张宇:"+i);
// }
// }
// };
//‐‐‐这个整体 相当于new MyRunnable()
Runnable r = new Runnable(){
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println("张宇:"+i);
}
}
//r可以替换成 new Runnable(){}
new Thread(r).start();
};
42. 线程安全
单线程程序不会出现线程安全
多线程程序,没有访问共享数据,不会产生问题
解决线程安全问题的一种方案:==使用同步代码块==
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
注意:
- 通过代码块中的锁对象,可以使用任意的对象
- 但是必须保证多个线程使用的锁对象是同一个
- 锁对象的作用:把同步代码块锁住,只让一个线程在同步代码块中执行
同步锁: 对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.
- 锁对象 可以是任意类型。
- 多个线程对象 要使用同一把锁。
注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着 (BLOCKED)
解决线程安全问题的第二种方案:==同步方法==
同步方法:使用==synchronized==修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
public synchronized void method(){
可能会产生线程安全问题的代码
}
使用步骤
- 吧访问了共享数据的代码抽取出来,放到一个方法中
- 在方法上添加synchronized修饰符
同步锁是谁?
- 对于非static方法,同步锁就是this,就是实现类对象 new RunnableImpl()。
- 对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。
解决线程安全问题的第二种方案:==Lock锁==
java.util.concurrent.locks.Lock接口
Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作
Lock接口中的方法:
void lock()获取锁
void unlock()释放锁
java.util.concurrent.locks.ReentrantLock implements Lock接口
使用步骤
- 在成员为止创建一个ReentrantLock对象
- 在可能会出现安全问题的代码前调用Lock接口中的方法获取lock获取锁
- 在可能会出现安全问题的代码后调用Lock接口中的方法获取unlock获取锁
unlock()方法可以放在finally中提高效率
43. 线程状态
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操 作系统处理器。 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状 态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个 状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态 将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、 Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
进入到TimeWaiting(计时等待)有两种方式
- 使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
- 使用wait(long m)方发,wait方法如果在毫秒值结束后,还没被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态
44. 线程间通信
等待唤醒机制
- wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时 的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象 上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
- notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先 入座。
- notifyAll:则释放所通知对象的 wait set 上的全部线程。
注意: 哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而 此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调 用 wait 方法之后的地方恢复执行。
总结如下:
- 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
- 否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态
调用wait和notify方法需要注意的细节
- wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对 象调用的wait方法后的线程。
- wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继 承了Object类的。
- wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。
45. 线程池
线程可以复用,执行完一个线程,不用销毁,可以继续执行其他任务
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源。
Java里面线程池的顶级接口是 java.util.concurrent.Executor ,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是 java.util.concurrent.ExecutorService 。
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。
参数: int nThreads->创建线程池中包含的线程数量
返回值: ExecutorService接口->返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)
(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
获取到了一个线程池ExecutorService对象,
那么怎么使用呢,
在这里定义了一个使用线程池对象的方法如下: public Future<?> submit(Runnable task) :获取线程池中的某一个线程对象,并执行 Future接口:用来记录线程任务执行完毕后产生的结果。
线程池的使用步骤
- 使用线程池的工厂类Executors里面提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
- 创建一个类,实现Runnable接口,重新run方法,设置线程任务
- 调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
- 调用ExecutorService中的方法shutdown销毁线程池(不建议使用)
Runnable实现类代码:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一个教练");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教练来了: " + Thread.currentThread().getName()); System.out.println("教我游泳,交完后,教练回到了游泳池");
}
}
线程池测试类:
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
// 创建Runnable实例对象
MyRunnable r = new MyRunnable();
//自己创建线程对象的方式
// Thread t = new Thread(r);
// t.start(); ‐‐‐> 调用MyRunnable中的run()
// 从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(r);
// 再获取个线程对象,调用MyRunnable中的run()
service.submit(r);
service.submit(r);
// 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
// 将使用完的线程又归还到了线程池中
// 关闭线程池
//service.shutdown();
}
}
网友评论