开始
线程: 线程是操作系统能够进行运算调度的最小单位, 线程被包含在进程之中, 是进程中的实际运作单位
线程可被描述为应用软件中互相独立, 可以同时运行的功能
并发/并行
- 并发: 同一时刻, 有多个指令在单个 CPU 上交替执行
- 并行: 同一时刻, 有多个指令在多个 CPU 上同时执行
Java 多线程
Thread 类
继承 Thread 类, 重写 run()
方法
public class JThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("This is " + this.getName());
}
}
}
JThread thread1 = new JThread();
// 为线程命名
thread1.setName("Thread 1");
// 开启线程
thread1.start();
线程对象会有默认名字, 格式为 Thread-[number]
Thread 的常用成员方法
方法 | 返回值 | 说明 |
---|---|---|
getName() | String | 获取线程名称 |
setName(String name) | void | 设置线程名称 |
static currentThread() | Thread | 获取当前线程 |
static sleep(long time) | void | 线程休眠, 单位为毫秒 |
setPriority() | void | 设置线程优先级 |
getPriority() | int | 获取优先级 |
setDaemon(boolean on) | void | 设置为守护进程 |
static yield() | void | 让出线程 |
static join() | void | 插入线程 |
Runnable Interface
实现 Runnable
接口
public class JRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("This is " + Thread.currentThread().getName());
}
}
}
可通过 Thread
类提供的静态方法 currentThread()
获取当前线程对象
将实现的接口放入 Thread
对象中并运行
Runnable jr = new JRunnable();
Thread t1 = new Thread(jr);
Thread t2 = new Thread(jr);
t1.setName("Thread 1");
t2.setName("Thread 2");
t1.start();
t2.start();
Callable Interface
与 Thread
和 Runnable
不同, Callable
的线程函数可返回值
实现 Callable
接口
public class JCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i < 100; i++) {
sum += i;
}
return sum;
}
}
创建 Callable
的实现对象, 然后创建 FeatureTask
对象和 Thread
对象
开启线程, 并通过 FeatureTask
获取线程运行结果
JCallable jCallable = new JCallable();
FutureTask<Integer> futureTask = new FutureTask<>(jCallable);
Thread thread = new Thread(futureTask);
thread.start();
Integer result = futureTask.get();
线程优先级
线程的调度式方式
- 抢占式调度 (Java 采用的调度方式)
- 非抢占式调度
setPriority()
方法设置线程优先级, 优先级从 1 ~ 10, 分十级, 线程默认优先级为5
优先级越高, 线程能抢到 CPU 的概率越大
守护线程
当其它的非守护线程执行完毕后, 守护线程会陆续结束
假设线程一运行时长为 2 秒, 线程二(守护线程)运行时间为 10 秒, 两个线程同时启动, 随着线程一运行完毕后, 线程二就会跟着关闭(不是立即关闭), 无论线程二是否运行完它的任务
更多
生命周期
线程安全
一个例子:
为一个线程对象设置 static
公共静态变量 count
, run()
方法是 while 循环自增 count
(期间打印 count
), 直到 count > 100
停止循环;
同时创建三个这样的线程, 并开始:
public class JLockThread extends Thread {
private static int count = 0;
public JLockThread() {
}
public JLockThread(String name) {
super(name);
}
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
if (count >= 100) break;
count++;
System.out.println(getName() + " | " + count);
}
}
}
若是写 while(count < 100)
, 数字不会超出 100
出现问题:
count
会重复出现相同的数字- 循环结束,
count
会超出 100
原因: 由于线程随时会抢夺 CPU 执行权, 单个线程的每个代码段执行权都有可能被其它线程抢走, 由于可能会出现三个进程同时执行在 count
自增的代码上 , 导致数字重复和数字超出
同步代码块 synchronized
为避免这个问题, 采用线程锁 synchronized
来限制线程执行权, 以此避免这个安全问题
通过线程锁, 其它线程必须等待当前线程执行完毕后, 才可执行
// 锁对象, 必须唯一
Object lock = new Object();
synchronized (lock) {
// 同步代码块
...
}
改进代码:
public class JLockThread extends Thread {
private static int count = 0;
private final static Object lock = new Object();
public JLockThread() {
}
public JLockThread(String name) {
super(name);
}
@Override
public void run() {
while (true) {
// 这里的锁对象 lock 也可以换成 JLockThread.class
synchronized (lock) {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
if (count >= 100) break;
count++;
System.out.println(getName() + " | " + count);
}
}
}
}
锁对象, 必须唯一
同步方法
同步方法就是将修饰符 synchronized
加到方法上, 用于锁住方法中的所有代码
synchronized void func(){
...
}
同步方法的锁对象不能被指定, 锁对象视情况而定:
- 方法非静态 - 锁对象为
this
- 方法静态 - 锁对象为当前类的字节码文件对象
public class JLockThread extends Thread {
private static int count = 0;
public JLockThread() {
}
public JLockThread(String name) {
super(name);
}
static synchronized boolean plus() {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
if (count >= 100) {
return true;
} else {
count++;
System.out.println(Thread.currentThread().getName() + " | " + count);
return false;
}
}
@Override
public void run() {
while (true) {
if (plus()) break;
}
}
}
Lock Interface
synchronized
修饰符不能手动释放锁, 因此 JDK5 引入 Lock
接口, 用于手动获得和释放锁
Lock
接口功能直接实例化, 可用它的实现类 ReentrantLock
来实例化
使用 Lock
要额外注意锁的释放, 一般做法是将 .unlock
方法放到 finally
代码块中, 保证锁能够完全释放
finally 有个特性, 就是无论如何 Java 最终都会执行里面的代码
死锁
不要写嵌套锁, 这样能最大程度的避免死锁
导致死锁的常见原因:
- 资源互斥
- 资源占有等待
- 不可抢占
- 循环等待
其它
线程安全的对象:
以 StringBuilder
与 StringBuffer
为例, 两个对象的成员方法一模一样, 其不同就是线程安全性
- StringBuffer 线程安全
- StringBuilder 线程不安全
如何选择?
单线程环境用 StringBuilder
, 多线程用 StringBuffer
, 就这么简单