Java 高级特性之多线程
=========================================================================
程序(program):程序是为完成特定任务、用某种语言编写的一 组指令的集合 。即指一段静态的代码,静态对象。
进程(process):进程是程序的一次执行过程,或是正在运行的一个程序 。是 一个动态的过程 :有它自身的产生、存在和消亡的过程,即生命周期。进程是作为资源分配的单位, 系统在运行时会为每个进程分配不同的内存区域。
线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。
若 一个进程同一时间并行执行多个线程,就是支持多线程的;
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器 ( pc),线程切换的开销小;
一个进程中的多个线程共享相同的内存单元/内存地址空间→它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
其中每个线程拥有自己独立的栈和程序计数器,其中多个线程共享同一个进程中的结构:方法区、堆。
并行:多个 CPU 同时执行多个任务。比如:多个人同时做不同的事 。
并发:一个 CPU( 采用时间片)同时执行多个任务。
需要同时执行两个或多个任务的程序;
需要实现一些需要等待的任务时,如用户输入、文件读写操作、网络操作、搜索等;
需要一些后台运行的程序。
一个 Java 应用程序其实至少三个线程:main()主线程、gc()垃圾回收线程和异常处理线程。
===============================================================================
创建多线程的方式有四种,分别是:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口、使用线程池。
四种方式的差异如下:
| 创建多线程的方式 | 特征 |
| --- | --- |
| 继承 Thread 类 | 使用时每次都要创建 Thread 类的子类的对象 |
| 实现 Runnable 接口 | 使用时创建实现类的对象,然后将此对象作为参数传递到 Thread 类的构造器中,从而创建 Thread 类的对象。相比于继承 Thread 类,1)实现的方式没有类的单继承性的局限性;2)多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。 |
| 实现 Callable 接口 | 相比于实现 Runnable 接口,1)call()可以有返回值的;2)call()可以抛出异常,被外面的操作捕获,获取异常的信息。而之前的方法只能是 try catch 捕获异常;3)Callable 是支持泛型的。 |
| 使用线程池 | 用从学校到西湖举例,前三种方法创建多线程就是每次去都要造一辆自行车,骑到西湖后就把自行车销毁,而线程池的方法就是搭乘公共交通去西湖。因此,这一种是最常用的方法,好处是:1)提高响应速度(减少了创建新线程的时间);2)降低资源消耗(重复利用线程池中线程,不需要每次都创建);3)便于线程管理。 |
下面用代码实例来讲解每一种方法,这样更容易理解。
=================================================================================
/**
需求:多线程的创建的方式二:实现 Runnable 接口
创建一个实现了 Runnable 接口的类
实现类去实现 Runnable 中的抽象方法:run()
创建实现类的对象
将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
通过 Thread 类的对象调用 start()
比较创建线程的两种方式。
开发中:优先选择:实现 Runnable 接口的方式
原因:1. 实现的方式没有类的单继承性的局限性
联系:public class Thread implements Runnable
相同点:两种方式都需要重写 run(),将线程要执行的逻辑声明在 run()中。
*/
public class BasicRunnable {
public static void main(String[] args) {
//3. 创建实现类的对象
Myhread myhread = new Myhread();
//4. 将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的对象
Thread t1 = new Thread(myhread);
t1.setName("线程 1");
//5. 通过 Thread 类的对象调用 start():① 启动线程 ②调用当前线程的 run()-->调用了 Runnable 类型的 target 的 run()
t1.start();
//再启动一个线程,遍历 100 以内的偶数
Thread t2 = new Thread(myhread);
t2.setName("线程 2");
t2.start();
//如下操作仍然是在 main 线程中执行的。
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i );
}
}
}
}
//1. 创建一个实现了 Runnable 接口的类
class Myhread implements Runnable{
//2. 实现类去实现 Runnable 中的抽象方法:run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
该程序运行后可以发现,子线程和主线程的运行顺序是随机的,这非常不好,因为当多个线程同时对一个共享数据进行操作时,会导致操作的不完整性,会破坏数据,从而使得程序不稳定。
如以下这个实例 BadTicket,模拟火车站售票程序,开启三个窗口售票。会发现 ticket 会有重复,而且还可能会负值,这是因为 t1、t2、t3 都进入了 if 判断语句,而没有走到 ticket- -。
class Window1 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(true){
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}else{
break;
}
}
}
}
public class BadTicket {
public static void main(String[] args) {
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口 1");
t2.setName("窗口 2");
t3.setName("窗口 3");
t1.start();
t2.start();
t3.start();
}
}
所以一方面我们会通过 synchronized 来同步线程,但其中有两种方式,一种是同步代码块,另一种是同步方法,这两种方式有些区别,我将分两段代码来介绍。另一方面,我们会使用 Lock 锁来解决这个问题。
3.2 利用 Runnable 实现线程同步(synchronized 同步代码块)
内容见注释,在代码中学习是最好的。
/**
需求:创建三个窗口卖票,总票数为 100 张。使用同步代码块解决实现 Runnable 接口的方式的线程安全问题
1.问题:卖票过程中,出现了重票、错票 -->出现了线程的安全问题
2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
3.如何解决:当一个线程 a 在操作 ticket(共享数据)的时候,其他线程不能参与进来。直到线程 a 操作完 ticket 时,其他
4.在 Java 中,我们通过同步机制,来解决线程的安全问题。
5.同步的方式,解决了线程的安全问题。---好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。 ---局限性
JDK5.0 之前
方式一:同步代码块
synchronized(同步监视器){
}
说明:1.操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
方式二:同步方法。
*/
public class SynchroRunnable {
public static void main(String[] args) {
Window1 w = new Window1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口 1");
t2.setName("窗口 2");
t3.setName("窗口 3");
t1.start();
t2.start();
t3.start();
}
}
class Window1 implements Runnable{
private int ticket = 100;
// Object obj = new Object();只需放在 run()方法的外面即可
@Override
public void run() {
// Object obj = new Object(); 放在这个地方就不安全了,要放在 run()方法的外面,因为每个线程都有自己的一把锁
while(true){
//此时的 this:唯一的 Window1 的对象或者使用一个 Object 对象的方法
synchronized (this){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
3.3 利用 Runnable 实现线程同步(synchronized 同步方法)
/**
利用同步方法的方式来解决线程安全
*/
public class SynchroMethodRunnable {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口 1");
t2.setName("窗口 2");
t3.setName("窗口 3");
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
//默认同步监视器:this
private synchronized void show(){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
3.4 利用 Runnable 实现线程同步(Lock 锁)
3.4.1 Lock 锁定义
从 JDK 5.0 开始,Java 提供了更强大的线程同步机制 通过显式定义同步锁对象来实现同步,同步锁使用 Lock 对象充当 。
java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象 。
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义, 在 实现线程安全的控制中,比较常用的是 ReentrantLock 可以显式加锁、释放锁。
3.4.2 代码实例
import java.util.concurrent.locks.ReentrantLock;
/**
解决线程安全问题的方式三:Lock 锁 --- JDK5.0 新增
面试题:synchronized 与 Lock 的异同?
相同:二者都可以解决线程安全问题
不同:synchronized 机制在执行完相应的同步代码以后,自动的释放同步监视器
2.优先使用顺序:
Lock ? 同步代码块(已经进入了方法体,分配了相应资源) ? 同步方法(在方法体之外)
*/
public class Lock {
public static void main(String[] args) {
Window3 w = new Window3();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口 1");
t2.setName("窗口 2");
t3.setName("窗口 3");
t1.start();
t2.start();
t3.start();
}
}
class Window3 implements Runnable{
private int ticket = 100;
//1.实例化 ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{//2.调用锁定方法 lock()
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
ticket--;
}else{
break;
}
}finally {
//3.调用解锁方法:unlock()
lock.unlock();
}
}
}
}
3.5 synchronized 同步与 Lock 锁的区别
相同点:二者都可以解决线程安全问题。
不同点:
synchronized 机制在执行完相应的同步代码以后,自动的释放同步监视器,Lock 是显式锁(需要手动开启和关闭锁), synchronized 是隐式锁,出了作用域自动释放;
Lock 只有代码块锁, synchronized 有代码块锁和方法锁;
使用 Lock 锁, JVM 将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)。
使用的优先顺序:Lock 锁、synchronized 同步代码块、synchronized 同步方法。
同步虽然解决了线程的安全问题,但其实操作同步代码时,只能一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
3.6.1 定义
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
出现死锁后,不会出现异常,不会出现提示,只是所的线程都处于阻塞状态,无法继续
我们使用同步时,要避免出现死锁。
3.6.2 解决办法
专门的算法、原则
尽量减少同步资源的定义
尽量避免嵌套同步
3.6.3 死锁的实例
//死锁的演示;死锁的增强版
class A {
public synchronized void foo(B b) { //同步监视器:A 类的对象:a
System.out.println("当前线程名: " + Thread.currentThread().getName()
" 进入了 A 实例的 foo 方法"); //
System.out.println("当前线程名: " + Thread.currentThread().getName()
" 企图调用 B 实例的 last 方法"); // ③
b.last();
}
public synchronized void last() {//同步监视器:A 类的对象:a
System.out.println("进入了 A 类的 last 方法内部");
}
}
class B {
public synchronized void bar(A a) {//同步监视器:b
System.out.println("当前线程名: " + Thread.currentThread().getName()
" 进入了 B 实例的 bar 方法"); // ②
System.out.println("当前线程名: " + Thread.currentThread().getName()
" 企图调用 A 实例的 last 方法"); // ④
a.last();
}
public synchronized void last() {//同步监视器:b
System.out.println("进入了 B 类的 last 方法内部");
}
}
public class DeadLock implements Runnable {
A a = new A();
B b = new B();
public void init() {
Thread.currentThread().setName("主线程");
// 调用 a 对象的 foo 方法
a.foo(b);
System.out.println("进入了主线程之后");
}
@Override
public void run() {
Thread.currentThread().setName("副线程");
// 调用 b 对象的 bar 方法
b.bar(a);
System.out.println("进入了副线程之后");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();
dl.init();
}
}
==============================================================================
/**
测试 Thread 中的常用方法:
start():启动当前线程;调用当前线程的 run()
run(): 通常需要重写 Thread 类中的此方法,将创建的线程要执行的操作声明在此方法中
currentThread():静态方法,返回执行当前代码的线程
getName():获取当前线程的名字
setName():设置当前线程的名字
yield():释放当前 cpu 的执行权
join():在线程 a 中调用线程 b 的 join(),此时线程 a 就进入阻塞状态,直到线程 b 完全执行完以后,线程 a 才
stop():已过时。当执行此方法时,强制结束当前线程。
sleep(long millitime):让当前线程“睡眠”指定的 millitime 毫秒。在指定的 millitime 毫秒时间内,当前
isAlive():判断当前线程是否存活
线程的优先级:
MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5 -->默认优先级
2.如何获取和设置当前线程的优先级:
getPriority():获取线程的优先级
setPriority(int p):设置线程的优先级
说明:高优先级的线程要抢占低优先级线程 cpu 的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下
被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
*/
public class ThreadMethod {
public static void main(String[] args) {
HelloThread h1 = new HelloThread("Thread:1");
h1.setName("线程一");
//设置分线程的优先级
h1.setPriority(Thread.MAX_PRIORITY);
h1.start();
//给主线程命名
Thread.currentThread().setName("主线程");
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
}
}
System.out.println(h1.isAlive());
}
}
class HelloThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
}
}
}
public HelloThread(String name){
super(name);
}
}
/**
需求:多线程的创建的方式一:继承于 Thread 类
步骤:
创建一个继承于 Thread 类的子类
重写 Thread 类的 run() --> 将此线程执行的操作声明在 run()中
创建 Thread 类的子类的对象
通过此对象调用 start()
说明各个子线程和主线程的顺序是随机的★
*/
public class BasicThread {
public static void main(String[] args) {
//3. 创建 Thread 类的子类的对象
MyThread t1 = new MyThread();
//4.通过此对象调用 start():①启动当前线程 ② 调用当前线程的 run()
t1.start();
MyThread t2 = new MyThread();
t2.start();
//如下操作仍然是在 main 线程中执行的。
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i );
}
}
}
}
//1. 创建一个继承于 Thread 类的子类
class MyThread extends Thread {
//2. 重写 Thread 类的 run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
4.3 利用 Thread 实现线程同步(synchronized 同步代码块)
/**
需求:创建三个窗口卖票,总票数为 100 张。使用同步代码块解决实现 Runnable 接口的方式的线程安全问题
1.问题:卖票过程中,出现了重票、错票 -->出现了线程的安全问题
2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
3.如何解决:当一个线程 a 在操作 ticket(共享数据)的时候,其他线程不能参与进来。直到线程 a 操作完 ticket 时,其他
4.在 Java 中,我们通过同步机制,来解决线程的安全问题。
5.同步的方式,解决了线程的安全问题。---好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。 ---局限性
JDK5.0 之前
方式一:同步代码块
synchronized(同步监视器){
}
说明:1.操作共享数据的代码,即为需要被同步的代码。 -->不能包含代码多了,也不能包含代码少了。
方式二:同步方法。
*/
public class SynchroThread {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口 1");
t2.setName("窗口 2");
t3.setName("窗口 3");
t1.start();
t2.start();
t3.start();
}
}
class Window implements Runnable{
private static int ticket = 100;
//一定要加 static 才可以
private static Object obj = new Object();
@Override
public void run() {
while(true){
//正确的 synchronized (obj)。在反射的时候会说,Window2.class 只会加载一次
synchronized (Window.class){
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}else{
break;
}
}
}
}
}
4.4 利用 Thread 实现线程同步(synchronized 同步方法)
/**
利用同步方法的方式来解决线程安全
*/
public class SynchroMethodThread {
public static void main(String[] args) {
Window2 w = new Window2();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口 1");
t2.setName("窗口 2");
t3.setName("窗口 3");
t1.start();
t2.start();
t3.start();
}
}
class Window2 implements Runnable {
private static int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
//必须使用静态方法,这样默认的同步监视器是 Window.class
private static synchronized void show(){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
=========================================================================
/**
需求:利用线程通信打印 1-100。关键点:线程 1, 线程 2 交替打印
涉及到的三个方法:
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify():一旦执行此方法,就会唤醒被 wait 的一个线程。如果有多个线程被 wait,就唤醒优先级高的那个。
notifyAll():一旦执行此方法,就会唤醒所有被 wait 的线程。
说明:
1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
否则,会出现 IllegalMonitorStateException 异常
3.wait(),notify(),notifyAll()三个方法是定义在 java.lang.Object 类中。
面试题:sleep() 和 wait()的异同?
1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:1)两个方法声明的位置不同:Thread 类中声明 sleep() , Object 类中声明 wait()
*/
public class CommunicationRunnable {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程 1");
t2.setName("线程 2");
t1.start();
t2.start();
}
}
class Number implements Runnable{
private int number = 1;
private Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj) {
obj.notify();
if(number <= 100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
try {
//使得调用如下 wait()方法的线程进入阻塞状态
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
评论