-
多线程
一、线程的基本概念
-
程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
-
进程(process):是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程——生命周期。
如:运行中的QQ、运行中的播放器。
程序是静态的,进程是动态的。
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
-
线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。
若一个进程同一时间并行执行多个线程,就是支持多线程。
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(PC),线程切换的开销小。
一个进程中的多个线程共享相同的内存单元/内存地址空间->它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
1、使用多线程有什么优点:
- 提高应用程序的响应,对图形化界面更有意义,可以增强用户的体验。
- 提高计算机系统CPU的利用率。
- 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改。
2、何时使用多线程
- 程序需要同时执行两个或多个任务。
- 程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等。
- 需要一些后台运行的程序时。
二、线程的创建和使用
方式一:
-
继承Thread类
-
重写Thread类的run()方法 ——>将此线程执行的操作声明在run()方法中
-
创建Tread类的子类对象
-
通过此对象调佣start()方法
例子:计算100以内的所有的偶数
class MyThread extends Thread{ @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0){ System.out.println(i); } } } } public class ThreadTest { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); //start()作用:①启动当前线程 ②调佣当前线程的run()方法 } }
// 使用多线程计算100以内的质数 class MyThread extends Thread{ @Override public void run() { boolean flag = true; for (int i = 2; i <= 100; i++) { for (int j = 2; j < i; j++) { // 除以这个数前面的数 if (i % j == 0){ //如果能被整除的话,则这个数不是质数 flag = false; } } if (flag){ System.out.println(i); } flag = true; } } } public class ThreadTest { public static void main(String[] args) { MyThread myThread = new MyThread(); myThread.start(); //start()作用:①启动当前线程 ②调佣当前线程的run()方法 } }
方式二:
-
创建一个实现了Runnable接口的类
-
实现类去实现Runnable中的抽象方法:run()
-
创建实现类的对象
-
将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
-
通过Thread类的对象调佣start()
例子:计算100以内所有的偶数
class MyThread1 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0){
System.out.println(i);
}
}
}
}
public class ThreadTest2 {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
Thread thread = new Thread(myThread1);
thread.start();
}
}
创建线程两种方式的比较:
优先选择实现Runnable接口的方式
- 实现的方式没有类的单继承性的局限性
- 实现的方式更适合来处理多个线程有共享数据的情况。
Thread中的常用方法:
- start():启动当前线程,调用当前线程的run()方法。
- run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中。
- currentThread():静态方法,返回执行当前代码的线程。
- getName():获取当前线程的名字。
- setName():设置当前线程的名字。
- yield():释放当前CPU执行权。
- join():在线程A中调用线程B的join()方法,此时线程A会进入阻塞状态,直到线程B完全执行之后,线程A才结束阻塞状态。
- stop():不建议使用。当执行此方法时,强制结束当前线程。
- sleep(long millitime):让当前线程“睡眠”指定的millitime毫秒。
- isAlive():判断当前线程是否存活。
线程的调度:
- 时间片策略:同优先级线程组成先进先出队列。
- 抢占式策略:对高优先级,使用优先调度的抢占式策略。
线程的优先级:
注意:并不是意味着当高优先级的线程被执行完成之后,低优先级的线程才被执行,而是高优先级的线程高概率被执行。
-
- MAX_PRIORITY:10
- MIN_PRIORITY:1
- NORM_PRIORITY:5 默认的线程优先级
-
如何获取和设置线程的优先级:
- getPriority():获取线程的优先级。
- setPriority(int p):设置线程的优先级 。
三、线程的生命周期
- 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能。
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。
四、线程的同步
例子:创建三个窗口卖票,总票数为100张。实现Runnable接口的方式。
问题:1.卖票的过程中出现了 重票、错票 -------> 线程的安全问题。
产生问题的原因:当某个线程操作车票的过程中,在没有完成的情况下,又有其他线程也参与进来。
如何解决:当一个线程正在操作共享数据的时候,其他线程不能参与出来,直到当前线程操作完成之后,其他线程才可以开始操作。
这种情况即使出现了阻塞,也不能被改变。
在java开发过程中:通过同步机制,来解决线程的安全问题。
方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码 说明:操作共享数据的代码,即为需要被同步的代码。
//同步监视器,俗称:锁;任何一个类的对象都可以充当锁。
//要求:多个线程必须要公用同一把锁。
}
class Window1 implements Runnable{
private int ticket = 100;
Object obj = new Object();
@Override
public void run() {
while (true){
synchronized (obj){
if (ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + "卖票,票号为:" + ticket);
ticket--;
}else{
break;
}
}
}
}
}
public class WindowTest1 {
public static void main(String[] args) {
Window1 window1 = new Window1();
Thread thread1 = new Thread(window1);
Thread thread2 = new Thread(window1);
Thread thread3 = new Thread(window1);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们可以把这个方法声明为同步的。
class Window2 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
private synchronized void show() {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + "卖票,票号为:" + ticket);
ticket--;
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
Window2 window2 = new Window2();
Thread thread1 = new Thread(window2);
Thread thread2 = new Thread(window2);
Thread thread3 = new Thread(window2);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
线程的死锁问题
死锁:
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
- 出现死锁之后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
解决方法:
- 专门的算法、原则。
- 尽量减少同步资源的定义。
- 尽量避免嵌套同步。
面试题:synchronized 与 Lock 的异同:
1. 相同点:二者都可以解决线程的安全问题。
2. 不同点:synchronized机制 在执行完相应的同步代码以后,自动的释放同步监视器,Lock需要手动的启动同步(lock()),同时结束同步也需要手动的去(unlock())。
推荐使用顺序:Lock—>同步代码块—>同步方法
解决线程的安全问题有几种方式:三种,同上顺序。
五、线程的通信
涉及的方法:
- wait():一旦执行此方法之后,当前线程就会进入阻塞状态,并释放同步监视器。
- notify():一旦执行此方法之后,就会唤醒被阻塞的线程。如果有多个线程被阻塞的话,则会唤醒优先级较高的线程。
- notifyAll():一旦执行此方法,就会唤醒所有被阻塞的线程。
注意:1. 这三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则,会报IllegalMonitorStateException的异常。
2. 这三个方法是定义在java.lang.Object类中的。
例子:使用两个线程打印 1-100。线程1, 线程2 交替打印
class PrintNum implements Runnable{
private int number = 1;
@Override
public void run() {
while (true){
//同步代码块,解决线程安全问题,this表示当前的对象,一定要唯一。
synchronized (this){
notify();//唤醒单个阻塞的线程
// notifyAll();//唤醒全部阻塞的线程
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (number <= 100){
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
//实现交替打印,当一个线程执行之后,让它进入阻塞状态。
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
PrintNum printNum = new PrintNum();
Thread thread1 = new Thread(printNum);
Thread thread2 = new Thread(printNum);
thread1.setName("线程1");
thread2.setName("线程2");
thread1.start();
thread2.start();
}
}
面试题:
1. sleep() 和 wait() 的异同:
-
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
-
不同点:Thread类中声明sleep()方法,Object类中声明wait()方法。
sleep()方法可以在任何需要的场景下调用,wait()方法必须在同步代码块或同步方法中调用。
如果是两个方法都是用在同步代码块或同步方法中,sleep()方法不会释放同步监视器,wait()方法会释放同步监视器。
JDK5.0之后新增线程的创建方式:
1.实现Callable接口
-
重写call()方法,可以有返回值。
-
方法可以抛出异常。
-
支持泛型的返回值。
-
需要借助FutureTask类,比如获取返回结果。
Future接口:
可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
FutrueTask是Futrue接口的唯一的实现类。
FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。
2. 使用线程池
提前创建多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池。
Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池。
Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池。
Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池。
Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
出处:https://www.cnblogs.com/whoops-zq/p/15060743.html