多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)
Java 通过 java.lang.Thread 类的对象来代表线程,main 方法也是一条线程,常作为主线程
①定义一个子类 MyThread 继承线程类 Thread,在类中重写 run() 方法
②创建 MyThread 类的对象
③在主线程 main 中调用线程对象的 start() 方法启动线程(执行run方法)
优点:编码简单
缺点:线程类已经继承 Thread,无法继承其他类(Java 不支持多继承),不利于功能的扩展
public class MyThread extends Thread{
@Override
public void run() {
for(int i = 0;i<5;i++){
System.out.println("MyThread:"+i);
}
}
}
public class Test {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
for(int i = 0;i<5;i++){
System.out.println("main:"+i);
}
}
}
注意:1)一定要用 start() 方法启用线程,如果使用 run() 方法,就变成了普通的方法调用
2)不要把主线程任务放到子线程之前,因为主线程不调用 start() 方法就不会启动子线程,最后导致主线程任务执行结束才执行子线程
①定义一个线程任务类 MyRunnable 实现 Runnable 接口, 重写 run() 方法
②创建 MyRunnable 任务对象
③把 MyRunnable 任务对象交给 Thread 对象接收,再调用 start() 方法
优点:任务类只是实现接口,可以继承其他类,或实现其他接口,扩展性强
缺点:需要多创建一个Runnable对象
public class MyRunnable implements Runnable{
@Override
public void run() {
for(int i = 0;i<5;i++){
System.out.println("子线程:"+i);
}
}
}
public class Test {
public static void main(String[] args) {
Runnable r = new MyRunnable();
new Thread(r).start();
for(int i = 0;i<5;i++){
System.out.println("main主线程:"+i);
}
}
}
补充:方式二的简化写法(利用匿名内部类和Lambda表达式)
public class Test {
public static void main(String[] args) {
new Thread(() -> {
for(int i = 0;i<5;i++){
System.out.println("子线程:"+i);
}
}).start();
for(int i = 0;i<5;i++){
System.out.println("main主线程:"+i);
}
}
}
①创建任务对象
定义一个类实现 Callable 接口, 重写 call 方法,封装要执行的任务和要返回的数据,把 Callable 类型的对象封装成 FutureTask (线程任务对象)
②把线程任务对象交给 Thread 对象。
③调用 Thread 对象的 start 方法启动线程。
④线程执行完毕后,通过 FutureTask 对象的的 get 方法去获取线程任务执行的结果
优点:线程任务类只是实现接口,可以继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果
public class MyCallable implements Callable {
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;
}
}
public class Test {
public static void main(String[] args) throws Exception{
Callable c1 = new MyCallable(100);
FutureTask f1 = new FutureTask<>(c1);
new Thread(f1).start();
Callable c2 = new MyCallable(200);
FutureTask f2 = new FutureTask<>(c2);
new Thread(f2).start();
String rs2 = f2.get();
System.out.println(rs2);
String rs1 = f1.get();
System.out.println(rs1);
}
}
注意:1)Callable 是泛型接口,FutureTask 是泛型类
2)在主线程中调用 get 方法时,如果当前子线程未执行完,则主线程会暂停等待
public class MyThread extends Thread{
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for(int i = 0;i<5;i++){
System.out.println(this.getName()+"=>"+i);
try {
Thread.sleep(1000);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
public class Test {
public static void main(String[] args) throws Exception{
Thread t1 = new MyThread("1号线程");
t1.setName("1号线程");
t1.start();
t1.join();
System.out.println(t1.getName());
Thread t2 = new MyThread("2号线程");
t2.setName("2号线程");
t2.start();
System.out.println(t2.getName());
Thread main = Thread.currentThread();
System.out.println(main.getName());
}
}
Thread 还有很多方法,这里不再介绍
1)存在多个线程在同时执行
2)同时访问一个共享资源
3)存在修改该共享资源
例:银行取钱模型
有两个人同时对同一个账户操作,该账户有10万元,取钱需要三个步骤:
1)输入取钱金额 2)判断余额是否足够 3)取出钱并更新账户余额
一个人要取6万元,另一个人要取8万元,在系统中他们同时输入金额,此时第一个人的系统网络较好,先判断了余额足够,但在他取出钱之前,第二个人也判断了余额足够,此时他们都取出了钱,导致银行账户凭空取出了4万元
以上述取钱模型为例:
1)创建一个账户类,用于模拟存钱和取钱
public class Account {
private double money;
public Account(double money) {
this.money = money;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public void drewMoney(double money){
String name = Thread.currentThread().getName();
System.out.println("欢迎您进入取钱系统,"+ name);
if(money < this.money){
System.out.println(name+"要取"+money+"元");
this.money -= money;
System.out.println(name+"成功取出"+money+"元,当前余额:"+this.money+"元");
}
else{
System.out.println("取钱失败,余额不足~~");
}
}
}
2)创建一个ATM类,用于模拟不同机器同时取钱(不同线程同时运作)
public class ATM extends Thread{
private Account acc;
private double money;
public ATM(String name, double money, Account acc) {
super(name);
this.money = money;
this.acc = acc;
}
@Override
public void run() {
acc.drewMoney(money);
}
}
3)在主线程中同时开启两个子线程
public class Test {
public static void main(String[] args) {
Account acc = new Account(100000);
new ATM("cxk",60000,acc).start();
new ATM("fzc",80000,acc).start();
}
}
4)结果如下(触发了线程安全问题)
线程同步:解决线程安全问题的方案
线程同步的思想:让多个线程实现以一定的先后顺序依次访问共享资源
加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能加锁进来
作用:把访问共享资源的核心代码给上锁,以此保证线程安全
原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
锁对象的规范:1)建议使用共享资源作为锁对象,对于实例方法建议使用 this 作为锁对象
2)对于静态方法建议使用字节码(类名.class)对象作为锁对象
public static void test(){
synchronized (Account.class){
}
}
public void drewMoney(double money){
String name = Thread.currentThread().getName();
System.out.println("欢迎您进入取钱系统,"+ name);
synchronized (this) {
if(money < this.money){
System.out.println(name+"要取"+money+"元");
this.money -= money;
System.out.println(name+"成功取出"+money+"元,当前余额:"+this.money+"元");
}
else{
System.out.println("取钱失败,余额不足~~");
}
}
}
注意事项:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug
作用:把访问共享资源的核心方法给上锁,以此保证线程安全
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行
public synchronized void drewMoney(double money){
String name = Thread.currentThread().getName();
System.out.println("欢迎您进入取钱系统,"+ name);
if(money < this.money){
System.out.println(name+"要取"+money+"元");
this.money -= money;
System.out.println(name+"成功取出"+money+"元,当前余额:"+this.money+"元");
}
else{
System.out.println("取钱失败,余额不足~~");
}
}
注意事项:同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码,默认与同步代码块的锁相同
补充:同步代码块和同步方法如何选择
范围上:同步代码块锁的范围更小,同步方法锁的范围更大,所以同步代码块在程序运行时更占优势
可读性上:同步方法更加简洁,可读性更好
4. Lock 锁
Lock 锁是 JDK5 开始提供的一个新的锁定操作,通过它可以创建锁对象进行加锁和解锁,更灵活、更方便、更强大
Lock 是接口,不能直接实例化,可以采用它的实现类 ReentrantLock 来构建 Lock 锁对象
private final Lock lock = new ReentrantLock();
public void drewMoney(double money){
String name = Thread.currentThread().getName();
System.out.println("欢迎您进入取钱系统,"+ name);
try {
lock.lock();
if(money < this.money){
System.out.println(name+"要取"+money+"元");
this.money -= money;
System.out.println(name+"成功取出"+money+"元,当前余额:"+this.money+"元");
}
else{
System.out.println("取钱失败,余额不足~~");
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
注意:1)在创建 Lock 锁对象时,尽量用 final 修饰(显得专业)
2)使用 unlock 方法解锁时,如果前面的代码出现了bug,则会导致无法正常解锁,所以要使用 finally 语句修饰 unlock 方法
当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺
生产者线程负责生产数据
消费者线程负责消费生产者生产的数据
生产者生产完数据后要等待,通知消费者消费;消费者消费完数据后也要等待,通知生产者生产
注意:1)上述方法应该使用当前同步锁对象进行调用(实例用 this,静态用类名.class)
2)一定要先唤醒其他线程,再等待自己,不然可能会出现死锁
线程池:一个可以复用线程的技术
作用:用户每发起一个请求,后台就需要创建一个新线程来处理,而创建新线程的开销很大,并且请求过多时,会产生大量的线程,这样会严重影响系统的性能。所以需要使用线程池通过复用线程来处理多个相同类型的任务
工作原理:线程池中分为两个部分
1)工作线程(WorkThread)
固定数量的线程,用来处理线程池接收的任务,当一个工作线程处理完当前任务时,会继续处理下一个任务,不再创建新线程
2)任务队列(WorkQueue)
可控制数量的任务对象,依次被工作线程处理,必须是由 Runnable 或 Callable 任务接口实现的
JDK 5.0起提供了代表线程池的接口:ExecutorService
1)使用 ExecutorService 的实现类 ThreadPoolExecutor 自创建一个线程池对象
参数一:corePoolSize:指定线程池的核心线程的数量
参数二:maximumPoolSize:指定线程池的最大线程数量(核心线程数 + 临时线程数)
参数三:keepAliveTime:指定临时线程的存活时间
参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)
参数五:workQueue:指定线程池的任务队列
参数六:threadFactory:指定线程池的线程工厂,用于创建线程
参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)
public class Test {
public static void main(String[] args) {
// public ThreadPoolExecutor(int corePoolSize,
// int maximumPoolSize,
// long keepAliveTime,
// TimeUnit unit,
// BlockingQueue workQueue,
// ThreadFactory threadFactory,
// RejectedExecutionHandler handler)
new ThreadPoolExecutor(3,5,8,TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}
注意:1)TimeUnit 是一个枚举类,包含了各种时间单位
2)WorkQueue 有两种表现形式:LinkedBlockingDeque (以链表形式存储,不限制大小),ArrayBlockingQueue (以数组形式存储,需要设置大小)
3)ThreadFactory 可以直接创建 ThreadFactory 对象然后重写 newThread 方法,也可以使用 Executors 工具类的 defaultThreadFactory 方法返回的线程工厂
4)hander 可以直接调用 ThreadPoolExecutor 类的静态方法 AbortPolicy(如果任务队列已满,则抛出异常)
2)使用 Executors (线程池的工具类)调用方法返回不同特点的线程池对象
注意:1)这些方法的底层,都是通过线程池的实现类 ThreadPoolExecutor 创建的线程池对象
2)在大型的并发环境中使用 Executors 可能会有系统风险
补充:创建线程池的某些原理问题
1)临时线程什么时候创建
新任务提交时发现核心线程都在忙,任务队列也满了,并且当前线程数小于最大线程数,此时才会创建临时线程
2)什么时候会开始拒绝新任务
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务
3)创建的线程池应该设置多少核心线程数
计算密集型任务:核心线程数量 = CPU内核数 + 1 IO 密集型任务:核心线程数量 = CPU内核数 * 2
1)处理 Runnable 任务
public class Test {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(3,5,8,TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
Runnable target = new MyRunnable();
try {
pool.execute(target); // 工作线程处理
pool.execute(target); // 工作线程处理
pool.execute(target); // 工作线程处理
pool.execute(target); // 填入任务队列,准备复用工作线程
pool.execute(target); // 填入任务队列,准备复用工作线程
pool.execute(target); // 填入任务队列,准备复用工作线程
pool.execute(target); // 任务队列已满,创建临时线程
pool.execute(target); // 任务队列已满,创建临时线程
pool.execute(target); // 任务队列已满,临时线程已满,抛出异常
} catch (Exception e) {
e.printStackTrace();
} finally {
pool.shutdown();
}
// pool.shutdownNow();
}
}
2)处理 Callable 任务
public class Test {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(3,5,8,TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
Callable c = new MyCallable();
try {
Future f1 = pool.submit(c);
Future f2 = pool.submit(c);
Future f3 = pool.submit(c);
Future f4 = pool.submit(c);
Future f5 = pool.submit(c);
Future f6 = pool.submit(c);
Future f7 = pool.submit(c);
Future f8 = pool.submit(c);
System.out.println(f1.get());
System.out.println(f2.get());
System.out.println(f3.get());
System.out.println(f4.get());
System.out.println(f5.get());
System.out.println(f6.get());
System.out.println(f7.get());
System.out.println(f8.get());
} catch (Exception e) {
e.printStackTrace();
}finally {
pool.shutdown();
}
// pool.shutdownNow();
}
}
注意:try 语句是用来捕获拒绝任务时抛出的异常,不然会导致线程池无法正常关闭
1)正在运行的程序( 软件)就是一个独立的进程
2)线程是属于进程的,一个进程中可以同时运行很多个线程
3)进程中的多个线程其实是由并发和并行执行的
1)一个处理器同时处理多个任务
2)逻辑上的同时:处理器以极快的速度不断轮换执行任务
1)多个处理器同时处理多个任务
2)物理上的同时:多个处理器同时处理一批任务,再同时切换到下一批任务
线程的各种状态的不断转变(从创建到结束)
线程的 6 种状态:Thread 类的内部枚举类中存储
直接认为此线程会修改公共资源,不加以判断直接加锁,使其他同类型线程排队
线程安全,但性能较差
上述的线程同步就是悲观锁的思想
public class MyRunnable implements Runnable{
private int count;
@Override
public void run() {
for(int i = 0;i<10;i++){
synchronized (this) {
System.out.println(Thread.currentThread().getName()+"count = "+(++count));
}
}
}
}
一开始不上锁,所有线程一起执行,等到出现线程不安全的问题时开始控制
线程安全,性能较好
public class MyRunnable implements Runnable{
// 整型乐观锁 -- 由原子类实现
private AtomicInteger count = new AtomicInteger();
@Override
public void run() {
for(int i = 0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"count = "+ count.addAndGet(1));
}
}
}
这里只做介绍,以后会深入学习