多线程
一:基本概念:程序,进程,线程
程序(program):程序是为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process):进程是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程:有它自身的产生,存在,消亡的过程。——生命周期。进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
线程(thread):进程可进一步细分为多个线程,是一个程序内部执行的一条路径。
- 若一个程序同一时间并行执行多个线程,则称该程序是支持多线程的
- 线程作为调度和执行的,每个线程拥有独立的运行栈和程序计数器(PC)
- 一个进程中的多个线程共享相同的内存单元/内存地址空间——堆和方法区是一个进程所拥有的,线程可以访问其中的对象和变量。这使得线程通信更加简便,有效,但同时多个线程共享的系统资源可能会带来安全隐患。
多线程程序的优点:
- 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
- 提高计算机系统CPU的利用率。
- 改善程序结构,将既长又复杂的进程分为多个线程,独立运行,有利于理解和修改。
何时需要多线程:
- 程序需要同时执行两个或多个任务
- 程序需要实现一些等待的任务时,如用户输入,文件读写操作,网络操作等
- 需要一些后台运行的程序
多线程的创建
1. 继承自Thread类
- 创建一个类继承自Thread类
- 重写Thread类的run()方法,run()方法中包含该线程需要执行的逻辑
- new一个Thread类的子类的对象
- 通过调用该对象的start()方法启动线程
class ChildThread extends Thread{@Overridepublic void run() {for (int i = 0; i < 100; i++) {if(i % 2 == 0){System.out.println("白");}}}
}
public class MyThread {public static void main(String[] args) {ChildThread t1 = new ChildThread();// start()方法有两个作用,启动线程,调用该线程的run方法// 若调用t1.run(),那么没有启动子线程,只是调用了对象的run()方法。// 一个线程对象只能start()一次t1.start();for (int i = 0; i < 100; i++) {if(i % 2 != 0) {System.out.println("梦" + "Main");}}}
}
Thread类中的常用方法:
- start():启动当前线程,并调用线程的run()方法
- run():通常需要重写Thread类中的run()方法,将创建的线程的执行逻辑写在此方法中
- currentThread:静态方法,获得当前正在执行的线程
- getName:返回当前正在执行线程的名字
- setName:设置当前线程的名字
- yield():释放当前CPU的执行权
- join():在线程a中执行线程b的join()方法,此时线程a进入阻塞状态,直到线程b执行完之后,线程a才结束阻塞状态
- sleep(Long millitime):让当前线程睡眠(阻塞)millitime毫秒
- isAlive:判断当前线程是否存活
线程的优先级:
-
优先级
-
MIN_PRIORITY = 1
-
NORM_PRIORITY = 5
-
MAX_PRIORITY = 10
-
-
获得和设置优先级
public final int getPriority() {return priority;}
public final void setPriority(int newPriority)
高优先级的线程可以抢占低优先级线程的资源,但并不绝对,只是说它拥有这个能力。
卖票实例(具有隐患):
// 多个线程共享资源带来了一定的安全隐患,后续解决class WindowDemo extends Thread{// ticket变量需声明为static,需要多个窗口(对象)共用一个变量private static int ticket = 100;@Overridepublic void run() {while (true){if(ticket > 0){System.out.println(getName() + ":" + "卖出票" + ticket);ticket--;}else{break;}}}
}
public class SellTicketDemo {public static void main(String[] args) {WindowDemo w1 = new WindowDemo();WindowDemo w2 = new WindowDemo();WindowDemo w3 = new WindowDemo();w1.setName("窗口1");w2.setName("窗口2");w3.setName("窗口3");w1.start();w2.start();w3.start();}
}
2.实现Runnable接口的类
- 创建一个实现Runnable接口的类
- 实现Runnable接口中的方法(run()方法)
- 创建一个该类的对象
- 将此对象作为参数传入到Thread类的构造器中,创建一个Thread对象
- 通过调用Thread对象的start方法,开启线程
// 同理,该类存在线程安全问题
class WindowDemo2 implements Runnable{private int ticket = 100;@Overridepublic void run() {while (true){if(ticket > 0){// 该类非Thread类的子类,所以需要调用完整的getName()方法System.out.println(Thread.currentThread().getName() + ":" + "卖出票" + ticket);ticket--;}else{break;}}}
}
public class SellTicketDemo2 {public static void main(String[] args) {WindowDemo2 windowDemo2 = new WindowDemo2();// 这里不需要将ticket声明为static,因为是使用同一个对象去创建的线程,三个线程访问同一个ticketThread t1 = new Thread(windowDemo2);t1.setName("窗口1");Thread t2 = new Thread(windowDemo2);t2.setName("窗口2");Thread t3 = new Thread(windowDemo2);t3.setName("窗口3");/*调用Thread类的start方法,而start方法又会调用Thread类的run(),为什么这里他去调用了实现*Runnable接口类的run()方法呢。因为Thread类的start()方法会先判断是否传入实现Runnable接口*的类,若是的话,则调用实现Runnable接口类的run()方法 */t1.start();t2.start();t3.start();}
}
比较创建多线程的两种方式:
- 开发中,优先选择使用第二种方式,实现Runnable接口的方式
- 实现接口的方式可以摆脱Java中类的继承的局限性
- 在需要开启多个线程,且这些线程拥有共享数据的时候,实现接口的方式更适合
- 实际上,Thread类就是实现Runnable接口的类,我们创建一个类去继承Thread类,并覆盖它的run()方法,归根结底还是实现Runnable接口中的run()方法
3. JDK5.0新增的两种方式
3.1 实现Callable接口的类
与Runnable相比,Callable的功能更加强大
- call方法可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
实现步骤:
- 创建一个实现Callable接口的实现类
- 实现类重写call方法,将此线程需要执行的逻辑写在call()方法中
- 创建实现类的一个对象
- 将实现类的对象作为参数传入到FutureTask的构造器中,创建一个FutureTask对象
- 将FutureTask对象作为参数传入到Thread类的构造器中,创建一个Thread对象,并调用start()方法
class ThreadTest implements Callable<Integer>{@Overridepublic Integer call() throws Exception {Integer sum = 0;for (int i = 1; i <= 100; i++) {if(i % 2 == 0){System.out.println("遍历到了" + i);sum += i;}}System.out.println("和为:" + sum);return sum;}
}
public class CallableDemo {public static void main(String[] args) {ThreadTest threadTest = new ThreadTest();// Future接口的唯一实现类,实现了Runnable接口和Callable接口FutureTask<Integer> integerFutureTask = new FutureTask<>(threadTest);new Thread(integerFutureTask).start();try {Integer ans = integerFutureTask.get();System.out.println(ans);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}}
}
3.2 使用线程池
思路:提前创建好多个线程放入线程池,使用时直接获取,使用完又放回到池中。这样可以避免线程多次创建销毁,进而实现重复利用。
好处:
-
提高响应速度(减少创建新线程的时间)
-
降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
-
便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- KeepAliveTime:线程没有任务时最多保持多久会终止
线程的生命周期
Thread.State类中定义了线程生命周期中的5个状态:
- 当一个Thread类或它的子类对象被生命并创建时,新生的线程对象处于新建状态(新建)
- 处于新建状态的线程调用start方法后,将进入线程队列,等待CPU时间片,此时它已经具备了运行的条件,只是还没被CPU分配内存资源(就绪)
- 当就绪的线程被被调度并且获得CPU资源时,便进入了运行状态,run()方法中定义了线程的操作和功能(运行)
- 在某种特殊情况下,被人为挂起或执行输入输出时,让出CPU并临时终止自己的执行,进入阻塞状态(阻塞)
- 线程完成了它的全部功能或线程被强制地终止或出现异常导致线程结束(死亡)
多线程的安全问题:
- 多个线程执行的不确定性引起执行结果的不稳定
- 多个线程对数据的共享,可能会对数据造成破坏
解决方法
解决:当一个线程在操作共享数据时,其它的线程不能够参与进来,直到该线程结束后,才允许其它线程对共享数据的操作。
在Java中,我们通过同步机制来解决线程安全的问题。
**方法一:**同步代码块
synchronized(同步监视器){
// 需要被同步的代码,(操作共享数据的代码),共享数据为多个线程共同操作的变量。
}
同步监视器俗称“锁”,任意一个对象即可。但是所有的存在安全隐患线程需要共同使用一个对象,即它们共同拥有一把锁。
**方法二:**同步方法
如果同步代码块被完全声明在一个方法中,我们可以将这个方法声明为同步的。
声明同步方法的方式是在方法的返回值前面加上synchronized关键字。
同步方法仍然会用到同步监视器,只不过不需要被主动的声明。
非静态的同步方法,同步监视器为this。该情况对应的是以Runnable接口的形式创建线程
静态的同步方法,同步监视器为当前类本身。该情况对应的是以Thread类子类的形式创建线程
同步的方式解决了线程安全的问题,但同时每次只能够有一个线程执行同步代码块,相当于又回到了单线程的过程(局限性)。
单例模式之懒汉式的线程安全
1. 什么是单例模式
单例模式是保证整个应用程序周期内,在任何时刻,被指定的类只能够有一个实例。
实现单例模式的方式:
- 构造方法声明为私有的,外界不能够调用构造方法构造对象
- 类本身需要构造一个对象——调用构造方法即可
- 通过公共的方法对外提供这个唯一的实例对象
2. 实例
// 外界只能通过getInstance()方法来获得Bank类的唯一对象
class Bank{private Bank(){};private static Bank instance = null;public static Bank getInstance(){// 该层if语句可以提高一定的效率, 因为同步操作实际上只需要进入的第一个// 线程操作。if(instance == null){// 同步代码块的方式synchronized(Bank.class){if(instance == null){instance = new Bank();}}}return instance;}
}
线程的死锁问题
- 死锁的理解
- 不同的线程占据着对方所需要的同步监听器不放弃,都在等待着对方所占据的自己所需要的同步监听器,进而形成了线程的死锁。
- 死锁出现后,程序并不会抛出异常,也不会出现提示,只是线程此时处于阻塞状态,无法继续。这不符合我们对于线程的定义,所有的线程最后都应该消亡。
- 解决办法
- 专门的算法,原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
死锁示例:
public class DeadLock {public static void main(String[] args) {StringBuilder s1 = new StringBuilder();StringBuilder s2 = new StringBuilder();new Thread(){@Overridepublic void run() {synchronized(s1){s1.append('a');s2.append(1);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s2){s1.append('b');s2.append(2);}}System.out.println(s1);System.out.println(s2);}}.start();new Thread(() -> {synchronized (s2){s1.append('c');s2.append(3);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (s1){s1.append('d');s2.append(4);}}System.out.println(s1);System.out.println(s2);}).start();}
}
Lock锁
Lock锁是JDK5.0新增的解决线程安全的方法。同时Lock是一个接口,我们一般使用它的实现类ReentrantLock类。 // 它自己就是一把抽象的锁。
class WindowDemo extends Thread{// ticket变量需声明为static,需要多个窗口(对象)共用一个变量// Lock锁同样要声明为静态的private static ReentrantLock lock = new ReentrantLock(true);private static int ticket = 100;@Overridepublic void run() {while (true) {lock.lock();try {if (ticket > 0) {System.out.println(getName() + ":" + "卖出票" + ticket);ticket--;} else {break;}}finally {lock.unlock();}}}
synchronized与Lock的区别:
-
synchronized的机制是在执行完同步代码块或方法后,自动释放同步监视器。
-
Lock需要手动的开启同步(Lock.lock()),同时在同步结束之后需要手动的结束结束同步(Lock.unlock())。
线程的通信
线程通信涉及到的三个方法:
- wait():执行此方法,当前线程将进入到阻塞状态,并且释放同步监视器。
- notify():执行此方法,将唤醒被wait()的一个线程,如果有多个线程被wait()则唤醒优先级高的那个线程。
- notifyAll():执行此方法,将唤醒所以被wait()的线程。
注意:
- 以上三个方法必须使用在同步代码块或同步方法中
- 以上三个方法的调用者必须是同步代码块或者同步方法的同步监听者
- 以上三个方法定在在java.lang.Object类中
sleep()方法和wait()方法的异同:
相同点:一旦执行上述方法,当前正在进行的线程将进入阻塞状态。
不同点:1)sleep()方法声明在Thread类中,为静态方法。wait()方法声明在Object()类中。
2)调用的要求不同,只要我们想要,随时可以通过Thread.sleep()调用sleep()方法。而wait()方法必须在 同步方法或同步代码块中调用。
3)调用wait()方法会释放同步监视器,而调用sleep()方法不会释放同步监视器。
生产者与消费者问题
有一个生产者可以一直生产产品,柜台工作人员可以从生产者这里得到产品,而消费者可以从柜台工作人员这里买到产品。要求唱片最多为66个,一旦有了66个产品,柜台工作人员就会通知生产者不要生产了,同时当没有产品得时候,柜台工作人员会告述消费者不要来买东西了,没有了。
分析:
- 多线程问题,生产者是一类线程,消费者是一类线程。
- 存在线程安全问题,它们之间有共享数据,柜台工作人员或者说是柜台工作人员手中的产品
/** Clerk类表示柜台工作人员,成员变量productNumber表示当前产品的个数。* 生产者可以到调用produce()方法表示生产一个产品* 消费者可以调用consume()方法表示买走一个产品* 生产者和消费者需要调用getProductNumber()方法才能得得当前的产品个数*/
class Clerk{private int productNumber = 0;public int getProductNumber() {return productNumber;}public void produce() {productNumber++;}public void consume() {productNumber--;}
}
class Producer implements Runnable{// 声明成员变量clerk,它是多线程的共同变量,可充当同步锁使用private Clerk clerk;// 声明构造方法从外部传入公共的clerkpublic Producer(Clerk clerk) {this.clerk = clerk;}@Overridepublic void run() {while (true) {synchronized (clerk) {if (clerk.getProductNumber() < 66) {// notify方法与wait方法的调用对象应该和同步锁一样,若不声明由谁调用方法,将会默认为this.方法clerk.notify();clerk.produce();System.out.println(Thread.currentThread().getName() + "生产产品:" + clerk.getProductNumber());try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}} else {try {clerk.wait();} catch (InterruptedException e) {e.printStackTrace();}}}}}
}
class Customer implements Runnable{private Clerk clerk;public Customer(Clerk clerk) {this.clerk = clerk;}@Overridepublic void run() {while (true) {synchronized (clerk) {if (clerk.getProductNumber() > 0) {// notify方法与wait方法的调用对象应该和同步锁一样,若不声明由谁调用方法,将会默认为this.方法clerk.notify();System.out.println(Thread.currentThread().getName() + "消费产品:" + clerk.getProductNumber());clerk.consume();try {Thread.sleep(200);} catch (InterruptedException e) {e.printStackTrace();}} else {try {clerk.wait();} catch (InterruptedException e) {e.printStackTrace();}}}}}
}
public class ThreadDemo {public static void main(String[] args) {Clerk clerk = new Clerk();Producer producer = new Producer(clerk);Customer customer = new Customer(clerk);Customer customer1 = new Customer(clerk);Thread t1 = new Thread(producer);Thread t2 = new Thread(customer);Thread t3 = new Thread(customer1);t1.setName("生产者1");t2.setName("消费者1");t3.setName("消费者2");t1.start();t2.start();t3.start();}
}