Java 线程 – 线程的创建、状态、方法和数据共享问题
- 多线程机制,目的就是为了
提高程序的处理效率
。
进程与线程
-
一个进程可以有很多线程,每条线程并行执行不同的任务。
-
单核的 CPU 不能够做到真正的多线程并发,
在某一个时间点上实际上只能处理一件事情
。利用 CPU 极快的处理速度,将多个线程之间频繁切换执行
,让外部感觉是多个事情同时在做。
进程(Process)
-
进程:
一段程序的执行过程
。就是正在运行中的程序。(狭义) -
进程:是
一个具有一定独立功能的程序关于某个数据集合的一次运行活动
。(广义) -
是系统
执行资源分配和调度的独立单位
。 -
每一个进程都有
独属于自己的存储空间
和系统资源
。 -
进程是
驻留在内存中
的。
线程(Thread)
-
线程:
进程中一个单一顺序的控制流
。 -
操作系统能够进行运算调度的最小单位
。 -
线程被包含在进程之中,
是进程中的实际运作单位
。 -
线程是
独立调度和分派的基本单位
。 -
同一个进程内的线程
共享该进程的全部系统资源
,使用同一个堆内存
和方法区内存
。 -
每一个线程有一个独立的
栈空间
,互不干扰。
线程的状态及方法
-
新建状态:该线程对象刚被创建出来。
-
就绪状态(可运行状态):表示当前线程
具有抢夺 CPU 时间片的权力(CPU 时间片就是执行权)
。- 当一个线程抢夺到 CPU 时间片之后,就开始执行 run 方法,
run 方法的开始执行标志着线程进入运行状态
。
- 当一个线程抢夺到 CPU 时间片之后,就开始执行 run 方法,
-
运行状态:
获取到 CPU 资源后进入运行状态
。执行 run 方法中的逻辑代码。 -
阻塞状态(等待阻塞):阻塞状态是线程因为某种原因
放弃 CPU 使用权,暂时停止运行
。-
等待阻塞:
接收用户键盘输入、或者执行 sleep 方法
等,此时线程会进入阻塞状态。 -
锁池(同步阻塞):
运行的线程在获取对象的同步锁时,需要等待别的线程释放锁
,JVM 会把该线程放入锁池中。
-
-
死亡状态:
线程 run 方法运行完毕,该线程结束
。
线程的常用方法
-
线程优先级:最低优先级 1、默认优先级是 5、最高优先级 10。
-
线程默认名称:Thread-0、Thread-1、…、Thread-n。
方法名 | 方法类型 | 作用 |
---|---|---|
start() | 实例方法 | 启动一个线程分支,开辟一个新的栈空间;线程从 新建状态、或阻塞态 转到 就绪态 |
run() | 实例方法 | 线程从 就绪态 转到 运行态 |
getName() | 实例方法 | 获取当前线程名称 |
setName(String) | 实例方法 | 设置当前线程名称 |
setPriority(int) | 实例方法 | 设置当前线程的优先级 |
getPriority() | 实例方法 | 获取当前线程的优先级 |
interrupt() | 实例方法 | 终止线程的休眠 |
join() | 实例方法 | 将一个线程合并到当前线程中,当前线程受阻塞,加入的线程执行直到结束 |
wait() | 实例方法 | 等待,线程进入阻塞状态,等待同步锁 |
notify()、notifyAll() | 实例方法 | 通知,线程释放同步锁,通知正在等待的线程 |
sleep(long) | 静态方法 | 让当前线程休眠 n 秒 |
currentThread() | 静态方法 | 获取当前线程对象 |
yield() | 静态方法 | 让位方法,当前线程暂停,回到就绪状态,让给其它线程 |
线程的创建
-
Runnable 接口,线程接口类,只有一个必须实现的 run 方法。
-
Thread 类,线程类,实现了 Runnable 接口,拥有线程所有常用的方法,同时 run 丰富也是必须实现的。
-
Callable<> 枚举接口,线程接口类,只有一个 call 方法。
-
线程池只能放入实现 Runnable 或 Callable<> 类线程,不能直接放入继承 Thread 的类线程
。 -
Callable<> 接口的 call()方法允许抛出异常,而 Runnable 接口的 run()方法的异常只能在内部 try 处理
。
新建类
继承 Thread 类
-
创建线程类:
MyThread thread1 = new MyThread("线程 1");
。 -
run() 方法是必须实现
的,线程调用要用thread1.start()
方法。 -
MyThread 的类对象不能放入线程池,不支持。
-
没有返回值
。
public class TestThread {
public static void main(String[] args) {
// 创建线程
MyThread thread1 = new MyThread("线程 1");
// 启动线程
thread1.start();
// 调用方法,不是启动线程
// thread1.run();
MyThread thread2 = new MyThread("线程 2");
thread2.start();
}
}
class MyThread extends Thread {
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
IntStream.range(0, 5).forEach(i -> {
try {
// 休眠 0.2 秒
Thread.sleep(200L);
} catch(InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(this.getName() + ",这是第 " + i + "次。");
}
});
}
}
实现 Runnable 接口
-
创建线程类:
MyRunnable runnable = new MyRunnable(); Thread thread1 = new Thread(runnable, "线程 1");
。 -
run() 方法是必须实现
的,线程调用要用thread1.start()
方法。 -
MyRunnable 类对象可以放入线程池,同时推荐使用线程池的方式创建线程,不建议使用 new Thread() 的方式。
-
``没有返回值`。
public class TestThread {
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
// 创建线程(显式,不推荐)
Thread thread1 = new Thread(runnable, "线程 1");
// 启动线程
thread1.start();
Thread thread2 = new Thread(runnable, "线程 2");
thread2.start();
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
IntStream.range(0, 8).forEach(i -> {
try {
// 休眠 0.5 秒
Thread.sleep(500L);
} catch(InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + ",这是第 " + i + "次。");
}
});
}
}
实现 Callable<> 接口
-
创建线程类:
Thread thread1 = new Thread(task, "线程 1");
。 -
call() 方法是必须实现
的,线程调用要用thread1.start()
方法。 -
MyCallable 类对象可以放入线程池,同时推荐使用线程池的方式创建线程,不建议使用 new Thread() 的方式。
-
有返回值
。 -
call 方法可以抛出异常,而 run 方法不可以,只能在方法内部处理
。 -
Callable<> 和相应的任务类都在 java.util.concurrent 包下。
public class TestThread {
public static void main(String[] args) {
MyCallable callable = new MyCallable();
// 创建任务
FutureTask<Integer> task = new FutureTask<>(callable);
// 创建线程(显式,不推荐)
Thread thread1 = new Thread(task, "线程 1");
// 启动线程
thread1.start();
Integer obj = null;
try {
// 获取线程结果(会阻塞线程)
obj = task.get();
} catch(InterruptedException | ExecutionException e) {
e.printStackTrace();
}
System.out.println("线程执行结果:" + obj);
// 注意,同一个任务不会执行两次
FutureTask<Integer> task2 = new FutureTask<>(callable);
Thread thread2 = new Thread(task2, "线程 2");
thread2.start();
Integer obj2 = null;
try {
// 获取线程结果(会阻塞线程)
obj2 = task2.get();
} catch(InterruptedException | ExecutionException e) {
e.printStackTrace();
}
System.out.println("线程执行结果:" + obj2);
}
}
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int integer = 0;
for(int i = 0; i < 8; i++) {
// 休眠 0.5 秒
Thread.sleep(500L);
System.out.println(Thread.currentThread().getName() + ",这是第 " + i + "次。");
integer += i;
}
return integer;
}
}
- 注意:
Callable<> 线程接口的发挥结果是 Future<> 接口下的实现类
。
内部类
- 就是
以内部类的方式实现一个线程类
。
匿名内部类
-
适用于:
关键代码只执行一次,无需重复执行
的情况。 -
因为,这个内部类是匿名的,根本没有办法再次调用。
-
匿名内部类是:
new Runnable(){}
。Runnable 类内实现 run 方法即可。 -
为什么不使用 Callable<> 类呢?
使用匿名方式创建的任务无法用于输出结果
。
public class TestThread {
public static void main(String[] args) {
// 创建线程(显式,不推荐)
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
IntStream.range(0, 4).forEach(i -> {
try {
// 休眠 1 秒
Thread.sleep(1000L);
} catch(InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + ",这是第 " + i + "次。");
}
});
}
}, "线程 1");
}
}
内部类
-
适用于:
关键代码只是在某个类中执行,其他类不会执行
、该类一次只执行一次逻辑代码
的情况。 -
内部类是:
new Thread(){}
。
public class TestThread {
private static final Thread DEMO_THREAD = new Thread("线程 demoThread") {
@Override
public void run() {
IntStream.range(0, 5).forEach(i -> {
try {
// 休眠 0.7 秒
Thread.sleep(700L);
} catch(InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(this.getName() + ",这是第 " + i + "次。");
}
});
}
};
public static void main(String[] args) {
// 启动线程
DEMO_THREAD.start();
}
}
线程安全
- 《Java并发编程实践》中对线程安全的定义:
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
-
换句话说就是:
线程安全的对象被多个线程调用时,都等获得正确的结果
。 -
实现方案:
-
加锁(如:synchronized)
:给代码块或方法加上锁。 -
使用 Lock 锁机制
:对线程不安全的代码块或方法采用 lock() 加锁,使用 unlock() 解锁。
-
共享数据
- 同样,主要就是要
实现数据共享安全
。
方法一:子线程内 数据共享
-
就是:
将共享数据放到 子线程类内部
。 -
注意:
要对同一个线程类创建多个线程
。 -
同时对数据更改代码添加 synchronized 锁,添加 方法锁亦可
。
public class ShareThread {
public static void main(String[] args) {
ShareRunnable shareRunnable1 = new ShareRunnable();
Thread thread1 = new Thread(shareRunnable1, "线程 1");
thread1.start();
Thread thread2 = new Thread(shareRunnable1, "线程 2");
thread2.start();
// 验证结果
System.out.println(shareRunnable1.date);
try {
Thread.sleep(10000);
} catch(InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(shareRunnable1.date);
}
}
}
class ShareRunnable implements Runnable {
public int date = 100;
@Override
public void run() {
IntStream.range(0, 8).forEach(i -> {
try {
// 对代码块加锁
synchronized(this) {
date--;
}
// 休眠 0.5 秒
Thread.sleep(500L);
} catch(InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + " 将 date 减一,这是第 " + i + "次。");
}
});
System.out.println("最后 date 为:" + date);
}
}
方法一:主线程中 数据共享
-
就是:
将共享数据放到 主线程类中
。 -
同时,
对主线程的数据更改方法添加 synchronized 锁
。 -
此时,
子线程调用主线程的方法
即可。 -
注意:
要对同一个线程类创建多个线程
。
public class ShareThread {
private static int sum = 100;
public static void main(String[] args) {
SShareRunnable sShareRunnable = new SShareRunnable();
Thread thread1 = new Thread(sShareRunnable, "线程 1");
Thread thread2 = new Thread(sShareRunnable, "线程 2");
thread1.start();
thread2.start();
// 验证结果
System.out.println(ShareThread.getSum());
try {
Thread.sleep(5000);
} catch(InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(ShareThread.getSum());
}
}
public synchronized static void subtract() {
sum--;
}
public static int getSum() {
return sum;
}
}
class SShareRunnable implements Runnable {
@Override
public void run() {
IntStream.range(0, 4).forEach(i -> {
try {
ShareThread.subtract();
// 休眠 0.5 秒
Thread.sleep(500L);
} catch(InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + " 将 sum 减一,这是第 " + i + "次。");
}
});
}
}