线程
线程(Thread)是一个程序内部的一条执行流程
程序中如果只有一条执行流程,那这个程序就是单线程的程序
多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度)
如何创建多条线程:Java是通过java.lang.Thread类的对象来代表线程的
多线程的创建方式一:继承Thread类
步骤
1.定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
2.创建MyThread类的对象
3.调用线程对象的strat()方法启动线程(启动后还是执行run方法)
public class ThreadTest1{
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
for (int i = 1; i <= 5; i++) {
System.out.println("主线程的方法执行了" + i);
}
}
}
优缺点
优点:编码简单
缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展
注意事项
1.启动线程必须是调用strat方法,不是调用run方法(调用run方法就相当于调用了一个普通的方法,而不会创建一条新的线程)
2.main方法是由一条默认的主线程负责执行,调用的线程称为子线程,主线程和子线程可以同时运行
3.不要把主线程任务放在启动子线程之前,否则在主线程任务执行完毕后才会启动子线程,相当于没有形成多线程
多线程创建方式二:实现Runnable接口
步骤
1.定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
2.创建MyRunnable任务对象(可以用多态的方式,Thread类实现了Runnable接口)
3.把MyRunnable任务对象交给Thread处理
|
Thread类提供的构造器 |
说明 |
|
public Thread(Runnable target) |
封装Runnable对象成为线程对象 |
4.调用线程对象的start()方法启动线程
public class ThreadTest2 {
public static void main(String[] args) {
MyRunnable target = new MyRunnable();
new Thread(target).start();
for (int i = 1; i <= 5; i++) {
System.out.println("主线程启动了" + i);
}
}
}
匿名内部类写法
public class ThreadTest2 {
public static void main(String[] args) {
//匿名内部类写法
Runnable target = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程启动了" + i);
}
}
});
new Thread(target).start();
//简化形式
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("子线程启动了" + i);
}
}
}).start;
for (int i = 1; i <= 5; i++) {
System.out.println("主线程启动了" + i);
}
}
}
该匿名内部类的形式除了以上的简化形式外还可以用Lambda表达式简化
优缺点
优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强
缺点:需要多一个Runnable对象
多线程的创建方式三:利用Callable接口、FutureTask类来实现
步骤
1.创建任务对象
定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据
把Callable类型的对象封装成FutureTask(线程任务对象)
2.把线程任务对象交给Thread对象
3.调用Thread对象的start方法启动线程
4.线程执行完毕后,可以通过FutureTask对象的get方法去获得线程任务执行的结果
注意事项
1.Callable是一个泛型类,可以定义返回的结果的类型
2.使用get方法得到的结果可能是异常,因此要处理异常
3.如果主线程调用get方法,但是子线程还没有执行完毕,主线程会先暂停,等到子线程执行完毕在获得结果
FutureTask的API
|
构造器 |
说明 |
|
public FutureTask<>(Callable call) |
把Callable对象封装成FutureTask对象 |
|
方法 |
说明 |
|
public V get() throws Exception |
获取线程执行call方法返回的结果 |
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class ThreadTest3 {
public static void main(String[] args) throws Exception {
Callable call = new MyCallable(100);
FutureTask f = new FutureTask<>(call);
new Thread(f).start();
//注:这里接收到的子线程的执行的结果可能是异常
//如果主线程到这里,子线程还没结束,会先暂停,等到子线程的执行完毕后才会获取结果
System.out.println(f.get());
}
}
优缺点
优点:
1.线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强。
2.可以在线程执行完毕后去获取线程执行的结果
缺点:编码复杂一点
Thread的常用方法
|
常用方法 |
说明 |
|
public void run() |
线程的任务方法 |
|
public void start() |
启动线程 |
|
public String getName() |
获取当前线程的名称,线程名称默认是Thread-索引 |
|
public void setName(String Name) |
为线程设置名称 |
|
public static Thread currentThread() |
获取当前执行的线程对象 |
|
public static void sleep(long time) |
让当前执行的线程休眠多少毫秒后,再继续执行 |
|
public final void join()... |
暂停主线程,等调用当前这个方法的线程执行完之后在开启主线程(如果之前有多个子线程再执行,然后一个子线程调用该方法,只会暂停主线程,另一个子线程不影响,两个子线程会一起执行) |
|
构造器 |
说明 |
|
public Thread(String name) |
可以为当前线程指定名称 |
|
public Thread(Runnable targer) |
封装Runnable对象成为线程对象 |
|
public Thread(Runnable targer,String name) |
封装Runnable对象成为线程对象,并指定线程名称 |
注意事项
我们自己创建的线程都是Thread类的子孙类,当我们用继承了Thread的类创建对象时,如果要在创建对象的时候为线程指定名称,可以在该线程类中加一个有参构造器,并直接调用父类的有参构造器[super(name)]
//直接调用父类的有参构造器
public MyThread(String name){
super(name);
}
线程安全
多个线程,同时访问同一个共享资源,且存在修改该资源
线程同步
认识线程同步
加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来
方式一、同步代码块
作用:把访问共享资源的核心代码上锁,以此包装线程安全
synchronized(同步锁){
访问共享资源的核心代码
}
原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
//1.同步代码块上锁
public void drawMoney(int number) {
synchronized (this) {
//1.判断余额是否足够
if (number > money){
System.out.println("余额不足");
return;
}
//让线程停一下,确保会出现线程安全问题
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.取钱
System.out.println(name + "成功取钱" + number + "元");
//3.更新账户
money -= number;
}
}
注意事项:
1.同心锁是一个对象
2.对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug
锁对象的使用规范
建议使用共享资源作为锁对象
1.对于实例方法建议用this作为锁对象
2.对于静态方法建议使用字节码(类名.class)对象作为锁对象
方式二、同步方法
作用:把访问共享资源的核心方法给上锁,依次保证线程安全
格式:
修饰符 synchronized 返回值类型 方法名称(形参列表){
操作共享资源的代码
}
原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行
//2.同步方法上锁
public synchronized void drawMoney(int number) {
String name = Thread.currentThread().getName();
//1.判断余额是否足够
if (number > money){
System.out.println("余额不足");
return;
}
//让线程停一下,确保会出现线程安全问题
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.取钱
System.out.println(name + "成功取钱" + number + "元");
//3.更新账户
money -= number;
}
底层原理:
同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码
如果方法是实例方法:同步方法默认用this作为的锁对象
如果方法是静态方法:同步方法默认用类名.class作为的锁对象
同步代码块和同步方法的比较
范围上:同步代码块锁的范围更小,同步方法锁的范围更大
性能上:同步代码块锁的范围小,因此性能会好一点(对于现在的计算机而言可以忽略)
可读性:同步方法更好
方式三、Lock锁
Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁
Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建锁对象
|
构造器 |
说明 |
|
public ReentrantLock() |
获得Lock锁的实现类对象 |
|
方法名 |
说明 |
|
void lock() |
获得锁 |
|
void unlock() |
释放锁 |
//3.Lock锁
public void drawMoney(int number) {
String name = Thread.currentThread().getName();
try {
//上锁
lk.lock();
//1.判断余额是否足够
if (number > money){
System.out.println("余额不足");
return;
}
//让线程停一下,确保会出现线程安全问题
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.取钱
System.out.println(name + "成功取钱" + number + "元");
//3.更新账户
money -= number;
} catch (Exception e) {
e.printStackTrace();
} finally {
//解锁
lk.unlock();
}
}
注意事项:
1.锁对象一般是专属的,可以用final修饰
2.如果锁住的代码出了异常,会直接停止运行,可能会导致无法释放锁,因此要使用try-catch-finally来捕捉异常并释放锁
线程通信(了解)
线程通信
当多个线程共同操作共享的资源时,线程通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源竞争
常见模型
生产者线程负责生产数据
消费者线程负责消费生产者生产的数据
注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,并通知生产者生产
Object类的等待和唤醒方法
|
方法名 |
说明 |
|
void wait() |
让当前线程等待并释放所占锁,直到另一个线程调用notify()方法或notifyAll()方法 |
|
void notify() |
唤醒正在等待的单个线程 |
|
void notifyAll() |
唤醒正在等待的所有线程 |
注意:
1.上述方法应该使用当前同步锁对象进行调用,否则会出bug
2.等待应该在唤醒后面,否则等待自己之后就无法执行唤醒操作
线程池
认识线程池
线程池就是一个可以复用线程的技术
不适用线程池的问题
用户每发起一个请求,后台就需要创建一个新线程来处理,且创建新线程的开销很大,当请求过多时,会产生大量线程,严重影响系统性能
线程池的工作原理
线程池里有一块区域来放置固定数量的线程,称之为工作线程(或核心线程);一块区域来放置要处理的任务,称之为任务队列,线程会不断处理任务,完成后再去处理下一个任务;
注:每个任务都是一个对象,这些对象必须实现任务接口(Runnable或Callable)
创建线程池
JDK5.0起提供了代表线程池的接口: ExecutorService
得到线程池对象
使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
ThreadPoolExecutor构造器
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,RejectedExeutionHandler handler)
参数一:corePoolSize:指定线程池的核心线程的数量
参数二:maximumPoolSize:指定线程池的最大线程数量(除了核心线程,还有临时线程)
参数三:keepAliveTime:指定临时线程存活的时间
参数四:unit:指定临时现场存货的时间单位(秒、分、时、天)(TimeUnit是一个枚举类,直接调用里面的对象即可)
参数五:workQueue:指定线程池的任务队列
参数六:threadFactory:指定线程池的线程工厂
参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务列表也满了的时候,新任务来了该怎么处理)
注意事项
1.临时线程什么时候创建?
新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程
2.什么时候会开始拒绝新任务?
核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务
注:正在被处理的任务不在任务队列中
3.线程池中核心线程的创建
当线程池接到新任务时,首先线程池会判断当前已创建的线程是否小于 corePoolSize (核心线程数),如果小于,则无论已创建的线程是否空闲,都会选择创建一个新的线程来执行该任务,直到已创建的线程等于核心线程数。
4.还可以使用Excutors(线程池的工具类)调用方法返回不同特点的线程池对象,但使用这些方法创建的线程池可能会出现系统风险,同时阿里巴巴手册中的规范也禁止使用该方法,因此开发中只使用ThreadPoolExecutor来创建线程池
线程池处理Runnable、Callable任务
ExecutorService的常用方法
|
方法名 |
说明 |
|
void execute(Runnable command) |
执行Runnable任务 |
|
Future<T> submit(Callable<T> task) |
执行Callable任务,返回未来任务对象,用于获取线程返回的结果 |
|
void shutdown() |
等全部任务执行完毕后,在关闭线程池 |
|
List<Runnable> shutdownNow() |
立即关闭线程池,停止正在执行的任务,并返回队列中未执行的任务 |
新任务拒绝策略
|
策略 |
说明 |
|
ThreadPoolExecutor.AbortPolicy |
丢弃任务并抛RejectedExecutionException异常,是默认的策略 |
|
ThreadPoolExecutor.DiscardPolicy |
丢弃任务,但是不抛出异常,是不推荐的做法 |
|
ThreadPoolExecutor.DiscardOldestPolicy |
抛弃队列中等待最久的任务,然后把当前任务加入队列中 |
|
ThreadPoolExecutor.CallerRunsPolicy |
有主线程负责调用任务的run()方法从而绕过线程池直接执行(由于主线程停下来执行任务,在该任务执行完毕前无法接收后面的任务) |
import java.util.concurrent.*;
public class ThreadPoolTest1 {
public static void main(String[] args) {
// //1.通过ThreadPoolExecutor创建一个线程池对象
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);//创建临时线程处理
pool.execute(target);//创建临时线程处理
pool.execute(target);//丢弃任务,抛出异常
// pool.shutdown();//等待线程池里的任务全部执行完毕后,关闭线程池
// pool.shutdownNow();//立即关闭线程池,不管任务是否执行完毕
}
}
并发、并行
进程
正在运行的程序(软件)就是一个独立的进程
线程是属于进程的,一个进程中可以同时运行很多个线程
进程中的多个线程其实是并发和并行同时执行的
并发
进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉二这些线程在同时执行,这就是并发
并行
在同一个时刻上,同时有多个线程在被CPU调度执行,这就是并行
线程的生命周期
线程的生命周期
就是线程从生到死的过程中,经历的各种状态及状态转换
Java线程的状态
Java总共定义了6种状态,这6种状态都定义在Thread类的内部枚举类中
|
线程状态 |
说明 |
|
New(新建) |
线程刚被创建,但是并未启动 |
|
Runnable(可运行) |
线程已经调用了start(),等待CPU调度 |
|
Blocked(锁阻塞) |
线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态 |
|
Waiting(无限等待) |
一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒 |
|
Timed Waiting(计时等待) |
同Waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Waiting状态,过了指定时间后会自动启动 |
|
Teminated(被终止) |
因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡 |
线程的6种状态互相转换

拓展:悲观锁、乐观锁
悲观锁
一上来就加锁,没有安全感,每次只能一个线程进入访问完毕后,在解锁。线程安全,性能较差
乐观锁
一开始不上锁,大家一起跑,等要出现线程安全的时候才开始控制。线程安全,性能较好
原理
1.在线程开始执行的时候,会先存储这个值的地址
2.存储这个值,称为原始值
3.然后对这个值进行修改并存储修改后的结果(没有给这个值赋值,只是存储了修改后的值),称为修改值
4.通过地址找到这个值现在是多少并和存储的原始值比较
5.如果相等,代表这个值没有被其他线程修改,立刻将修改值赋值给这个值,然后返回结果
6.如果不相等,代表这个值已经被其他线程修改过了,之前的修改值是不正确的,舍弃修改值和存储值,重新执行第二步,直到原始值和通过地址找到的这个值相等
例子
需求:一个变量,初始值为10,对这个变量加1,且有多个线程同时执行该任务
步骤:1.存储这个值的对象和地址(这个变量是一个对象中的实例变量,因此要存储对象和地址才能找到这个变量)
2.通过地址找到这个值,并用一个变量存储起来(后面称为原始值,方便讲述)
3.对这个值进行加一,并把加一后的值用一个变量存储(称为修改值)
4.通过地址找到这个值并和原始值比较
5.如果相等,立刻将修改值赋值给这个变量,返回结果的时候使用原始值进行处理返回(不能再通过地址找值,因为这个值可能已经又被其他线程处理)(也可以使用修改值直接返回,但这是一个Java的方法的原理,里面的修改值都是,原始值经过过处理,当作形参给下一个方法,因此没有存储修改值)
6.如果不相等,再次执行第二步,直到成功修改(例子中使用了do-while方法)
该例子的实际代码
以上例子是Java中AtomicInteger的一个方法incrementAndGet,以下是实际代码
AtomicInteger a = new AtomicInteger();
a.incrementAndGet();
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
//这是通过C语言完成的底部逻辑,不用管
//大致作用是通过var1对象和var2地址来找到要修改的值
//var5是之前存储的原始值
//var4是要修改多少,这里传过来的是1,即要修改的值加1
//如果通过地址找到的值和原始值相等,会将var5 + var4通过地址赋值给要修改的值
8万+

被折叠的 条评论
为什么被折叠?



