创建线程
有三种方式创建线程:1. 继承Thread类;2. 实现Runnable接口并实现run方法;3. 实现Callable接口
public class Main {
// main方法默认是一条主线程
public static void main(String[] args) {
// 创建子线程对象
Thread t = new MyThread();
// 启动子线程
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程输出:" + i);
}
}
}
class MyThread extends Thread{
// 必须重写run方法
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程输出:" + i);
}
}
}
public class Main {
public static void main(String[] args) {
// 创建任务对象
Runnable target = new MyRunnable();
// 启动子线程,可用匿名内部类和lambda表达式简化编程
new Thread(target).start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程输出:" + i);
}
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程输出:" + i);
}
}
}
前两种创建线程方式都存在一个问题,如果线程执行完后有一些数据要返回,他们重写的run方法均不能返回结果。以下方式可以解决。
public class Main {
public static void main(String[] args) throws Exception {
// 创建Callable对象
Callable myCallable = new MyCallable(100);
// 把Callable对象封装成FutureTask对象(任务对象)
// FutureTask实现了Runnable接口,可以调用对象的get方法获取call的返回值
FutureTask<String> f1 = new FutureTask<>(myCallable);
// 把任务对象交给Thread对象,启动子线程
new Thread(f1).start();
// 获取子线程返回结果
// 主线程执行到这里会等待子线程执行完毕,保证获取结果
String s = f1.get();
System.out.println(s);
}
}
class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return "子线程输出:1-" + n + "的和是" + sum;
}
}
注意:启动线程必须是调用start方法,不是调用run方法。直接调用run方法会被当成普通方法执行,此时还是单线程执行。只用调用了start方法后才是启动了一个新的线程。
优点 | 缺点 | |
---|---|---|
第一种 | 编程简单 | 由于继承Thread类,不能在继承其他类,不方便扩展功能 |
第二种 | 只是实现了Runnable接口,可以继承其他类,扩展性更强 | 没有明显缺点 |
第三种 | 可以继承其他类,扩展性更强,可以返回数据 | 编码复杂 |
Thread类的常用方法
Thread还提供了如yield、interrupt、守护线程、线程优先级等线程控制方法。
public class Main {
public static void main(String[] args) throws Exception {
Thread t1 = new MyThread();
t1.setName("1号线程");
t1.start();
System.out.println(t1.getName());// Thread-0
Thread t2 = new MyThread("2号线程");
t2.start();
System.out.println(t2.getName());// Thread-1
Thread thread = Thread.currentThread();
System.out.println(thread.getName());// main
}
}
class MyThread extends Thread {
public MyThread() {}
public MyThread(String name) {
super(name);
}
// 必须重写run方法
@Override
public void run() {
Thread thread = Thread.currentThread();
for (int i = 0; i < 5; i++) {
System.out.println(thread.getName() + "线程输出:" + i);
}
}
}
线程同步(解决线程安全问题——加锁)
场景:小明和小红是一对夫妻,他们同时去银行从他们的共同账户取钱10w,而这个账户只有10w。
不加锁的代码如下
public class Main {
public static void main(String[] args) throws Exception {
Account account = new Account("中国银行", 100);
new PeopleThread(account, "小明", 100).start();
new PeopleThread(account, "小红", 100).start();
}
}
public class PeopleThread extends Thread{
private Account acc;
private int money;
public PeopleThread(Account acc, String name, int money) {
super(name);
this.acc = acc;
this.money = money;
}
@Override
public void run() {
acc.drawMoney(money);
}
}
public class Account {
private String cardId;
private int money;
public void drawMoney(int money) {
// 谁来取钱
String name = Thread.currentThread().getName();
if (this.money >= money) {
System.out.println(name + "来取钱"+ money);
this.money -= money;
System.out.println(name + "取走钱,剩余:" + this.money);
} else {
System.out.println(name + "来取钱,但是余额不足!!!");
}
}
public Account(String cardId, int money) {
this.cardId = cardId;
this.money = money;
}
public String getCardId() {
return cardId;
}
public void setCardId(String cardId) {
this.cardId = cardId;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
}
输出结果之一:
很明显银行亏钱了。
下面三种方式给以上程序枷锁,解决这个线程安全问题。
同步代码块
作用:把访问资源的核心代码给上锁,以此保证线程安全。
原理:每次只允许一个线程加锁后进入,执行完后立刻自动解锁,其他线程才可以进来执行。
对于当前执行的线程来说,同步锁必须是同一把(同一个对象),在案例中就像是小红和小明取钱的账户要是同一个,否则会出Bug。
只需要对drawMoney方法修改。
public void drawMoney(int money) {
// 谁来取钱
String name = Thread.currentThread().getName();
synchronized (this) {
if (this.money >= money) {
System.out.println(name + "来取钱"+ money);
this.money -= money;
System.out.println(name + "取走钱,剩余:" + this.money);
} else {
System.out.println(name + "来取钱,但是余额不足!!!");
}
}
}
建议使用共享资源作为锁对象,对于访问实例方法建议使用this作为锁对象。对于静态方法建议使用字节码(类名.class)对象作为锁文件。
同步方法
作用:把访问共享资源的核心方法给上锁。
原理:每次只允许一个线程加锁后进入,执行完后立刻自动解锁,其他线程才可以进来执行。
public synchronized void drawMoney(int money) {
// 谁来取钱
String name = Thread.currentThread().getName();
if (this.money >= money) {
System.out.println(name + "来取钱"+ money);
this.money -= money;
System.out.println(name + "取走钱,剩余:" + this.money);
} else {
System.out.println(name + "来取钱,但是余额不足!!!");
}
}
同步方法的底层原理:
- 同步方法底层也是有隐式锁对象的,只是锁的范围是整个方法。
- 实例方法,同步方法默认使用this作为锁对象。
- 静态方法,默认使用类名.class作为锁对象。
Lock锁
Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、方便、强大。
Lock是一个接口,不能直接被实例化,可以采用它的实现类ReentrantLock来构建锁对象。
修改Account中的代码。
// 创建一个锁对象
private final Lock lk = new ReentrantLock();
public void drawMoney(int money) {
// 谁来取钱
String name = Thread.currentThread().getName();
// 核心代码中可能有bug,异常抛出去了而没有解锁
try {
lk.lock();// 加锁
if (this.money >= money) {
System.out.println(name + "来取钱"+ money);
this.money -= money;
System.out.println(name + "取走钱,剩余:" + this.money);
} else {
System.out.println(name + "来取钱,但是余额不足!!!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lk.unlock();// 解锁
}
}
线程池
线程池就是一个可以复用线程的技术。
不使用线程池可能出现的问题:用户每发起一个请求,后台就需要创建一个新的线程出来,下次新任务来了又要创建新的线程。而创建线程的开销是很大的,并且请求过多时,会产生大量的线程出来,严重影响系统的性能。
线程池指定一片区域放置线程,还有一片区域放任务,当线程数量达到上限,新的任务就会排队等待线程忙完当前的任务。
我们把区域里的线程称为工作线程或者核心线程,这里面的线程是可以重复利用的。
任务队列里面的每一个任务都是一个对象,都实现了Runnable或Callable接口。
总之,线程池可以控制线程的数量来处理任务。
注意事项:
1、临时线程什么时候创建?
新的任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
2、什么时候开始拒绝新任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来时才会拒绝。
创建线程池(一)
代表线程池的接口:ExecutorService
方法一:使用ExecutorService的实现类ThreadPoolExecutor创建一个线程池对象
- 参数一:corePoolSize,指定线程池的核心线程数量
- 参数二:maximumPoolSize,指定线程池的最大线程数量
- 参数三:keepAliveTime,临时线程的存活时间
- 参数四:unit,临时线程的存活时间单位(秒、分、时、天)
- 参数五:workQueue,指定线程的消息队列
- 参数六:threadFactory,指定线程池的线程工厂,创建核心线程和临时线程
- 参数七:handler,指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新的任务来了怎么处理)
new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
以下常见的新任务拒绝策略:
处理线程池(一)
执行Runnable任务
public class Main {
public static void main(String[] args) throws Exception {
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
Runnable target = new MyRunnable();
pool.execute(target);// 创建一个核心线程,自动处理
pool.execute(target);// 创建一个核心线程,自动处理
pool.execute(target);// 创建一个核心线程,自动处理
pool.execute(target);// 任务队列排队等待
pool.execute(target);// 任务队列排队等待
pool.execute(target);// 任务队列排队等待
pool.execute(target);// 任务队列排队等待
// 到了创建临时线程的时机
pool.execute(target);// 创建临时线程
}
}
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "运行中");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "运行结束");
}
}
执行Callable任务
public class Main {
public static void main(String[] args) throws Exception {
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
Future<String> f1 = pool.submit(new MyCallable(100));
Future<String> f2 = pool.submit(new MyCallable(100));
Future<String> f3 = pool.submit(new MyCallable(100));
Future<String> f4 = pool.submit(new MyCallable(100));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
}
}
class MyCallable implements Callable<String> {
private int n;
public MyCallable(int n) {
this.n = n;
}
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return Thread.currentThread().getName() + "输出:1-" + n + "的和是" + sum;
}
}
创建线程池(二)
方法二:使用Executors(线程池工具类)调用方法返回不同特点的线程池对象
注意:这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。
用法大同小异。
public class Main {
public static void main(String[] args) throws Exception {
// ExecutorService pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,
// new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),
// new ThreadPoolExecutor.AbortPolicy());
ExecutorService pool = Executors.newFixedThreadPool(3);
Future<String> f1 = pool.submit(new MyCallable(100));
Future<String> f2 = pool.submit(new MyCallable(100));
Future<String> f3 = pool.submit(new MyCallable(100));
Future<String> f4 = pool.submit(new MyCallable(100));
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
}
}
值得注意的是:
- 底层都是实现类ThreadPoolExecutor
- 核心线程数设置多少?计算密集型的任务:CPU的核数 + 1;IO密集型的任务:CPU的核数 * 2。
- 大型并发系统环境中使用Executors如果不注意可能会出现系统风险。
细节知识
并发和并行
并发:并发是指两个或多个事件在同一时间间隔发生。
并行:并行是指两个或者多个事件在同一时刻发生。
从微观的层面来说,并发在某一时刻只在执行一个事件。
线程的生命周期