多线程学习
线程介绍
原来的方法:只有主线程一条执行路径,调用其他方法时先处理其他方法,之后再回来继续主线程;
多线程:主线程中调用start()方法,使得子线程开始执行,此时主线程不必等待,而是与子线程并行交替执行
其他概念对比:
- 程序:静态概念,是指令和数据的有序集合。
- 进程:动态概念,是执行程序的一次过程,它代表系统资源分配的单位。
- 线程:包含在进程之中,一个进程可以包含若干个线程,线程则是CPU调度和执行的单位。
注:
- 真正的多线程指多个CPU即多核处理;若是一个CPU模拟出来的多线程,在同一个时间点,CPU只能执行一个代码,但是线程之间互相切换很快,所以就显得是同时执行。
- 线程不一定会执行,这个由CPU决定调度执行
线程创建
1.方式分类
Thread class——继承Thread类Runnable接口——实现Runnable接口Callable接口——实现Callable接口
2.继承Thread类
步骤如下:
- 自定义线程类,并继承Thread类
- 重写run()方法,编写线程执行体
- 创建线程对象,调用start()方法启动线程
示例如下:
//继承Thread类
public class Demo01 extends Thread{
//重写run()
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hello");
}
}
public static void main(String[] args) {
Demo01 demo01 = new Demo01();
demo01.start();//执行子线程
for (int i = 0; i < 200; i++) {
System.out.println("hi");
}
}
}
当执行次数较多,可以观察到子线程和主线程穿插输出。
以下为案例示范:
//案例,线程同时下载文件
public class Demo02 extends Thread{
String url;
String file;
//构造方法初始化
public Demo02(String url, String file) {
this.url = url;
this.file = file;
}
//重写run(),一个线程执行y下载方法
@Override
public void run() {
Downloader downloader = new Downloader();
downloader.download(url,file);
System.out.println(file);
}
public static void main(String[] args) {
//URL是什么可以自己试验
Demo02 demo021 = new Demo02("https://*****","1.jpg");
Demo02 demo022 = new Demo02("https://*****","2.jpg");
Demo02 demo023 = new Demo02("https://*****","3.jpg");
demo021.start();
demo022.start();
demo023.start();
}
}
class Downloader{
//下载方法
public void download(String url, String file){
try {
FileUtils.copyURLToFile(new URL(url),new File(file));
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.实现Runnable接口
大致方式为:创建一个实现类实现Runnable接口,重写run()方法,用Thread()有参构造引入实现类的对象,并调用Thread对象中的start()方法即可实现多线程。
public class Demo03 implements Runnable{
//实现Runnable接口,重写run方法,最后调用start方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hello");
}
}
public static void main(String[] args) {
Demo03 demo03 = new Demo03();
new Thread(demo03).start();//调用start方法
for (int i = 0; i < 200; i++) {
System.out.println("hi");
}
}
}
4实现Callable接口(非重点应用)
方式类似,只是调用多线程服务时需要四步:
- 创建执行服务
- 提交执行
- 获取结果
- 关闭服务
public class Demo05 implements Callable {
String url;
String file;
public Demo05(String url, String file) {
this.url = url;
this.file = file;
}
//重写run()
@Override
public Boolean call() {
CallDownloader downloader = new CallDownloader();
downloader.download(url,file);
System.out.println(file);
return true;
}
public static void main(String[] args) throws Exception{
Demo05 demo021 = new Demo05("https://gimg2.baidu.com/***","1.jpg");
Demo05 demo022 = new Demo05("https://gimg2.baidu.com/*****","2.jpg");
Demo05 demo023 = new Demo05("https://gimg2.baidu.com/****","3.jpg");
//创建执行服务
ExecutorService ser = Executors.newFixedThreadPool(3);
//提交执行
Future<Boolean> r1 = ser.submit(demo021);
Future<Boolean> r2 = ser.submit(demo022);
Future<Boolean> r3 = ser.submit(demo023);
//获取结果
boolean rs1 = r1.get();
boolean rs2 = r2.get();
boolean rs3 = r3.get();
System.out.println(rs1);
System.out.println(rs2);
System.out.println(rs3);
//关闭服务
ser.shutdown();
}
}
class CallDownloader{
//下载方法
public void download(String url, String file){
try {
FileUtils.copyURLToFile(new URL(url),new File(file));
} catch (IOException e) {
e.printStackTrace();
}
}
}
静态代理
将对象区分为真实对象和代理对象。
- 他们继承同一个接口。
- 代理对象代理真实对象
所谓代理的意思就是,我代替其他人做那些做不了的事情,代理对象则就是去做真实对象做不了的事情。
如何实现?这里打个比方:
class A implements face//真实
{
@override
public void hhh(){
//***
}
}
class B implements face//代理
{
face fa;
public B(face fa){
this.fa = fa;
}
@override
public void hhh(){
//***
fa.hhh();
}
}
interface face{
public void hhh();
}
这里真实和代理都实现一个接口,但是两者的实现方法不同,且代理方法在实现过程中也实现真实对象的方法
最后在调用实现时:
new B(new A()).hhh();
就可以实现静态代理,这与多线程中:
new Thread(() -> System.out.println("")).start();
有异曲同工之妙
Lambda表达式
在上面说到多线程里面的时候有提到一个奇怪的表达式:
() -> System.out.println("").start()
这就是一种Lambda表达式
首先明确一个概念:函数式接口
任何接口,如果只包含一个抽象方法,那么就是一个函数式接口
当我们有了函数式接口才能用Lambda表达式:
public class Demo06 {
//Lambda表达式
public static void main(String[] args) {
testFace tf = () -> System.out.println("hhh");
tf.test();
}
}
//函数式接口
interface testFace{
public void test();
}
该式子是建立在匿名内部类基础上的改编,可以将其类比为:
new testFace(
@override
public void test(){
System.out.println("hhh");
})
详细解析一下:
1.()里面的是指该函数式接口的形参列表,如果形参没有就为空。如果只有一个形参可以不打括号,如果没有或者多个形参必须打括号。(形参数据类型可以省略)
2.箭头后面加大括号{},里面就是重写函数体,如果只有一句代码就不用打大括号,如上述例子所示。
线程状态相关
线程分为五个状态:创建态,就绪态,阻塞态,运行态,终止态
相关方法:
| 方法 | 说明 |
|---|---|
| setPriority(int newPriority) | 更改线程的优先级 |
| static void sleep(long millis) | 指定毫秒数内休眠该执行中的线程 |
| void join() | 等待该线程终止 |
| static void yield() | 暂停该执行中的线程,并执行其他线程,即线程礼让 |
| void interrupt() | 中断线程(不推荐) |
| boolean isAlive() | 检查线程是否存活 |
线程停止
一般建议让线程自己停止,实在不行可以考虑使用一个外部标志位让线程停止:
boolean flag = true;
public void stop(){
flag = false;
}
此时设立该flag变量作为外部标志位,当主线程和子线程一同变化的时候,在主线程中可以找机会使用stop()改变flag,以使得子线程停止运行。
线程休眠
Thread.sleep(int millis)方法,他存在异常InterruptedException,需要进行抛出。
注意:
- sleep方法会让线程进入阻塞态,进行完毕则进入就绪态
- sleep可以模拟网络延时,倒计时等
- 每个对象都有一个锁,sleep方法不会释放锁
线程礼让
礼让是让线程从运行转为就绪态,方法为Thread.yield()
礼让是不一定成功的!具体看CPU安排
线程强制执行
令其他线程阻塞,等待该线程执行完再执行其他线程,方法为join()
该方法不是静态方法!
观测线程状态
getState()方法,观察线程状态,也不是静态方法:
public static void main(String[] args) throws Exception{
//观察线程状态
//创建线程
Thread thread = new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("hhhh");
});
//观察状态
Thread.State state = thread.getState();
System.out.println(state);
//启动线程
thread.start();
//观察启动后线程的状态
state = thread.getState();
System.out.println(state);
while (state != Thread.State.TERMINATED){
//只要线程不终止,就一直输出c状态
Thread.sleep(100);
state = thread.getState();
System.out.println(state);
}
}
线程的优先级
java提供一个线程调度器监控启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。
使用setPriority,getPriority改变线程或者获取优先级
其中源码内几个静态常量:
public final static int MIN_PRIORITY = 1;//最小优先级
public final static int NORM_PRIORITY = 5;//中等优先级
public final static int MAX_PRIORITY = 10;//最大优先级
优先级高的越容易先执行,但是实际情况是CPU自己调动,只是调用概率高一点。
守护线程(daemon)
线程分为用户线程和守护线程
虚拟机需要确保用户线程执行完毕,但是不用等待守护线程执行完毕。虚拟机确定用户线程执行完毕即可关闭,守护线程在关闭虚拟机之时也会自行关闭。
public class Demo08 {
//守护线程
public static void main(String[] args) {
God god =new God();
//设置守护线程
Thread thread = new Thread(god);
thread.setDaemon(true);//守护线程,默认false指用户线程
//启动
thread.start();//守护线程
new Thread(() -> {
for (int i = 0; i < 300; i++) {
System.out.println("live is good!");
}
System.out.println("寄");
}).start();//用户线程
}
}
//守护线程类
class God implements Runnable{
@Override
public void run() {
while (true){
System.out.println("hi!!!!");
}
}
}
线程同步
即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态.
——百度百科
要实现线程同步的安全性:队列 + 锁(synchronized)
队列很简单理解,先入先出,类似于排队;锁则是每个线程都可以加的,开启锁之后,该线程独占资源,处理完该线程后释放锁,队列的下一个线程才能运行。
Synchronized
锁机制:
- 保证安全性,但是其他线程会挂起,丧失性能
- 优先级高的线程等待优先级低的线程释放锁,会导致优先级倒置,引起性能问题
要实现同步机制,就是要实现synchronized关键字,包括:同步方法和同步块。
要注意,synchronized关键字锁的对象必须是变化的量,例如:
- 同步方法:锁住运行的方法,当其他的线程需要调用这个方法时要看是否被锁
- 同步块:锁住某个对象,让块中的步骤在运行期间锁住该对象。
格式:
- 同步方法:前面加个synchronized关键字即可
- 同步块:synchronized(Obj)
注:
之前提到过ArrayList集合是不安全集合,在JUC包下有一个线程安全的对应集合类叫:CopyOnWriteArrayList。线程安全也就是指线程能够同步。
死锁
多个线程各自占有一些资源,并相互等待其他释放占有的资源。
在操作系统中,死锁必须满足四个条件:
- 互斥条件:某资源同一时间内只能由一个进程占有。
- 不剥夺条件:资源未用完前,其他进程不能剥夺资源,只能由本进程自己释放。
- 请求并保持:进程已经占有至少一个资源,但又提出新的资源请求。
- 循环等待条件:存在一种进程链,使得链中每个进程已获得的资源被下一个进程请求。
在JAVA中表现为,某一个同步块同时拥有“两个以上对象的锁”,就会发生死锁问题。
Lock锁——显示锁
JUC包下的Lock接口是控制多个线程对共享资源进行访问的工具,提供了对共享资源的独占访问。
一般使用的实现类为:ReentrantLock——可重入锁,它拥有与Synchronized相同的并发性和内存语义。
public class TestLock {
public static void main(String[] args) {
TestLock2 lock2 = new TestLock2();
new Thread(lock2).start();
new Thread(lock2).start();
new Thread(lock2).start();
}
}
class TestLock2 implements Runnable{
int ticketsNum = 10;
//定义一个lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try{
lock.lock();//加锁
if (ticketsNum > 0){
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketsNum--);
}
else {
break;
}
}
finally {
lock.unlock();//解锁
}
}
}
}
注意:为了保证同步代码的异常抛出,最好将unlock()解锁方法写入finally语句块,所以以上代码内有try finally块在内。
Lock与synchronized锁区别:
- 显式Lock锁手动开启和关闭,synchronized隐式开启,超出作用域自动释放。
- Lock只能锁代码块,synchronized除此之外有方法锁
- Lock花费更少时间调度进程,性能更好,且提供更多子类,有可扩展性。
优先使用Lock锁进行加锁,其次使用同步代码块,最次用同步方法加锁
线程通信
生产者消费者问题
这是一个线程同步问题,生产者消费者共享一个资源,并且生产者和消费者之间相互依赖,互为条件。在这里面还要处理一种线程通信的问题:
- 对生产者,没有产品时要通知消费者等待;有产品时通知消费者消费
- 对消费者,消费之后要通知生产者已经结束消费,需要生产新产品以供消费
在这种问题内仅有synchronized是不够的,因为不能实现线程的通信。
线程通信的方法:
-
wait()——表示线程会等待,知道其他线程通知,与sleep方法不同的是它会释放锁。wait(long timeout)——指定等待的毫秒数 -
notify()——唤醒一个等待进程notifyAll()——唤醒同一对象上所有wait的线程,优先级高的先调度
解决消费者生产者问题的方法:
-
管程法
新建一个缓冲区
生产者用来生产数据,消费者用来处理数据,在生产者和消费者之间放入缓冲区,生产者将数据放入缓冲区,消费者从缓冲区取出数据。
-
信号灯法
使用一种并发协作模型处理:生产者/消费者模式,使用一个判断标志位监听是否传递。
管程法
//生产者消费者问题
//管程法
public class Demo10 {
public static void main(String[] args) {
SynContainer container = new SynContainer();
Producer producer = new Producer(container);
Consumer consumer = new Consumer(container);
new Thread(producer).start();
new Thread(consumer).start();
}
}
//生产者
class Producer implements Runnable{
SynContainer container;
public Producer(SynContainer container){
this.container = container;
}
//生产
@Override
public void run() {
for (int i = 0; i < 100; i++) {
container.synProducer(new Chicken(i));
System.out.println("生产了第"+i+"只鸡");
}
}
}
//消费者
class Consumer implements Runnable{
SynContainer container;
public Consumer(SynContainer container){
this.container = container;
}
//消费
@Override
public void run() {
for (int i = 0; i < 100; i++) {
Chicken chicken = container.synConsumer();
System.out.println("消费了第"+chicken.id+"只鸡");
}
}
}
//产品
class Chicken{
int id;//产品编号
public Chicken(int id){
this.id = id;
}
}
//缓冲区
class SynContainer{
//容器大小
Chicken[] chickens = new Chicken[10];
//计数器
int count = 0;
//生产者判断,记得加锁
public synchronized void synProducer(Chicken chicken){
//如果缓冲区满,就提醒消费者
while (count == chickens.length){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果没满就加入
chickens[count++] = chicken;
this.notifyAll();
}
//消费者判断,记得加锁
public synchronized Chicken synConsumer(){
//如果缓冲区为空,则等待生产者生产
while (count == 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//否则消费
Chicken chicken = chickens[--count];
this.notifyAll();
return chicken;
}
}
由于要实现线程同步,所以要加锁,但是不能使用sleep方法休眠程序,因为sleep休眠不释放锁,wait方法可以释放锁,从而让对方可以开始执行进程而不至于一直等待。
信号灯法
//生产者消费者问题
//信号灯法
//车辆通行,行人等待;车辆等待,行人通行
public class Demo11 {
public static void main(String[] args) {
Light light = new Light();
new Thread(new Car(light)).start();
new Thread(new Person(light)).start();
}
}
class Car implements Runnable{
Light light;
public Car(Light light){
this.light = light;
}
@Override
public void run() {
for (int i = 0; i < 30; i++) {
light.ride();
}
}
}
class Person implements Runnable{
Light light;
public Person(Light light){
this.light = light;
}
@Override
public void run() {
for (int i = 0; i < 30; i++) {
light.walk();
}
}
}
//红绿灯
class Light{
//true 行人通行
//false 车辆通行
boolean light = true;
public synchronized void walk(){
//如果false,就等待
if (!light){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果true就通行
notifyAll();
System.out.println("行人通行");
light = !light;
}
public synchronized void ride(){
//如果true,就等待
if (light){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果true就通行
notifyAll();
System.out.println("车辆通行");
light = !light;
}
}
线程池
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回,避免重复创建销毁,实现重复利用。
- 可以提高响应速度
- 降低资源消耗
- 便于管理:如线程池大小,最大线程数,线程池一起销毁判断
线程池接口:ExcutorService,常见子类ThreadPoolExcutor
方法如下:
void excutor(Runnable command):执行任务/命令,没有返回值,一般用作执行Runnable<T> Future<T> submit(Callable<T> task):一般用来执行Callable,有返回值
Excutors则是一种工具类,用于创建并返回不同类型的线程池:
public class Demo12 {
public static void main(String[] args) {
//创建线程池
ExecutorService service = Executors.newFixedThreadPool(10);//参数为池子大小
//线程加入池子并执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
//关闭链接
service.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
注:之前说的Callable接口那里有说过线程池,可以查看用法。

浙公网安备 33010602011771号