Java多线程,创建线程的3种方式、设计模式-静态代理模式、Lamda表达式,线程状态和常用方法,线程同步synchronized和Lock、死锁,线程协作:生产者消费者、管程法、线程池
概念线程就是独立的执行路径;在程序运行时,即使没有自己创建程,后台也会有多个线程,如主线程,gc线程(垃圾回收线程)main()称之为主线程,为系统的入口,用于执行整个程序;在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的。对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;线程会带来额外的开销,如cpu调度时间,并发控
这篇博客是看的这个网课做的笔记,很有收获:https://www.bilibili.com/video/BV1V4411p7EF Java多线程详解 非常感谢up:遇见狂神说
目录
概念
- 线程就是独立的执行路径;在程序运行时,即使没有自己创建程,后台也会有多个线程,如主线程,gc线程(垃圾回收线程)
- main()称之为主线程,为系统的入口,用于执行整个程序;
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为的干预的。
- 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
- 线程会带来额外的开销,如cpu调度时间,并发控制开销。
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
创建线程
创建线程主要有3种方式:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
继承thread类
自定义线程类继承Thread类
重写run()方法,编写线程执行体
创建线程对象,调用start()方法启动线程
如上图,run方法就是普通的方法调用,调用start方法是开启线程。
线程开启不一定立即执行,是由CPU调度执行。
//自定义线程类继承自Thread
private static class threadTest extends Thread{
@Override
public void run() {
super.run();
//重写run()方法,编写线程执行体
for (int i = 0; i < 20; i++) {
System.out.println("多线程");
}
}
public static void main(String[] args) {
//创建一个线程对象
threadTest tt = new threadTest();
//调用start方法开启线程
tt.start();
for (int i = 0; i < 2000; i++) {
System.out.println("主线程"+i);
}
}
}
实现Runnable接口
定义MyRunnable类,实现Runnable接口
实现run()方法,编写线程执行体
创建线程对象,调用start()方法启动线程
//定义MyRunnable类实现Runnable接口
private static class threadTest implements Runnable{
//实现run()方法,编写线程执行体
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("多线程");
}
}
public static void main(String[] args) {
//创建线程对象,调用start()方法启动线程
threadTest tt = new threadTest();
//正常的写法
// Thread thread = new Thread(tt);
// thread.start();
new Thread(tt).start();//简化的写法
}
}
更推荐使用实现Runnable接口来创建线程,因为Thread也是实现了Runnable接口,使用Runnable可以避免单继承局限性,灵活方便。
而且方便同一个对象被多个线程使用,下面是一个例子:
//创建线程对象,调用start()方法启动线程
threadTest tt = new threadTest();
new Thread(tt,"黄牛1").start();//不同的线程,同一个资源
new Thread(tt,"黄牛2").start();//不同的线程,同一个资源
new Thread(tt,"黄牛3").start();//不同的线程,同一个资源
为什么Runnable要通过Thread来启动线程呢,这就要了解一下静态代理模式了。
设计模式-静态代理模式
真实对象和代理对象都要实现同一个接口。
代理要代理真实角色,代理对象可以做真实对象做不了的事情,真实对象专注做自己的事情。
就像下面这个例子,婚礼新人You和婚礼代理公司WeddingCompany都继承了结婚marry这个接口,各自实现了HappyMarry()抽象方法,最后使用的时候,是将You对象传入WeddingCompany对象中,通过WeddingCompany的HappyMarry()方法完成的。
public static void main(String [] args){
//三对情侣结婚
new WeddingCompany(new You("A")).HappyMarry();
new WeddingCompany(new You("B")).HappyMarry();
new WeddingCompany(new You("C")).HappyMarry();
}
//结婚这件事情
private interface marry{
void HappyMarry();
}
//个人的结婚(穿戴好、执行婚礼仪式)
private static class You implements marry{
private String name;
public You(String n){
this.name = n;
}
@Override
public void HappyMarry() {
System.out.println(this.name+"新人结婚");
}
}
//婚礼代理公司的结婚(代理新人布置婚礼)
private static class WeddingCompany implements marry{
private marry target;
public WeddingCompany(marry target){
this.target = target;
}
@Override
public void HappyMarry() {
System.out.println("收钱");
System.out.println("布置婚礼现场,主持婚礼");
this.target.HappyMarry();
System.out.println("收尾款\n");
}
}
运行结果:
我们再看回Runnable,Thread和下面的threadTest都是实现了接口Runnable的类,Thread对象就是作为了threadTest的代理对象来使用的,有了这个代理对象,我们定义一个实现Runnable的类(比如说threadTest)的时候只要考虑最重要的事情(线程中要执行什么)来重写run方法,其他的事情用Thread代理对象来帮我们做。
public static void main(String[] args) {
//创建线程对象,调用start()方法启动线程
threadTest tt = new threadTest();
//正常的写法
// Thread thread = new Thread(tt);
// thread.start();
new Thread(tt).start();//简化的写法
}
Runnable还有更简化的写法,需要用到Lamda表达式:
Lamda表达式
Lambda 表达式主要用来定义行内执行的方法类型接口,例如,一个简单方法接口。
Lambda 表达式免去了使用匿名方法的麻烦,并且给予Java简单但是强大的函数化的编程能力。
例子:
最通常的使用接口的方法:
public class Test {
public static void main(String [] args) {
LamdaTest t = new Test1();//创建外部类对象(父类引用指向子类对象)
t.Lamda();//调用方法
}
}
//实现类
class Test1 implements LamdaTest{
@Override
public void Lamda() {
System.out.println("我是外部类");
}
}
//定义一个函数式接口(只有一个抽象方法)
interface LamdaTest{
void Lamda();
}
也可以放到类里,作为静态内部类:
public class Test {
public static void main(String [] args) {
LamdaTest t = new Test1();//创建外部类对象(父类引用指向子类对象)
t.Lamda();//调用方法
}
//实现类(静态内部类)
private static class Test1 implements LamdaTest{
@Override
public void Lamda() {
System.out.println("我是静态内部类");
}
}
}
//定义一个函数式接口
interface LamdaTest{
void Lamda();
}
也可以把类实现在方法内部,变成局部内部类:
public static void main(String [] args) {
//实现类(局部内部类)
class Test1 implements LamdaTest{
@Override
public void Lamda() {
System.out.println("我是局部内部类");
}
}
LamdaTest t = new Test1();//创建外部类对象(父类引用指向子类对象)
t.Lamda();//调用方法
}
//定义一个函数式接口
interface LamdaTest{
void Lamda();
}
也可以直接new接口然后实现方法,没有类名,变成匿名内部类:
public static void main(String [] args) {
//实现类(匿名内部类)
LamdaTest t = new LamdaTest(){
@Override
public void Lamda() {
System.out.println("我是匿名内部类");
}
};
t.Lamda();//调用方法
}
//定义一个函数式接口
interface LamdaTest{
void Lamda();
}
还可以更简化,使用Lamda表达式,是Java8的新特性:
public static void main(String [] args) {
//实现类
LamdaTest t = ()->{
System.out.println("我是Lamda表达式");
};
t.Lamda();//调用方法
}
//定义一个函数式接口
interface LamdaTest{
void Lamda();
}
如果要实现的方法中需要传参,括号中可以省略类型名:
public static void main(String [] args) {
//实现类(匿名内部类)
LamdaTest t = (a,b)->{
System.out.println("我是匿名内部类"+a+b);
};
t.Lamda(1,2);//调用方法
}
//定义一个函数式接口
interface LamdaTest{
void Lamda(int a,int b);
}
Runnable也是一个函数式接口,所以我们可以用Lambda 表达式来简化代码:
new Thread(()->{
System.out.println("Runnable线程运行");
}).start();
实现Callable接口
- 实现Callable接口,需要返回值类型
- 重写call方法,需要抛出异常
- 创建目标对象
- 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
- 提交执行:Future result1 = ser.submit(t1);
- 获取结果:boolean r1 = result1.get()
- 关闭服务:ser.shutdownNow();
public static void main(String [] args) throws ExecutionException, InterruptedException {
CallableTest t1 = new CallableTest();
CallableTest t2 = new CallableTest();
CallableTest t3 = new CallableTest();
//创建执行服务,线程池
ExecutorService es = Executors.newFixedThreadPool(3);
//提交执行
Future<Boolean> r1 = es.submit(t1);
Future<Boolean> r2 = es.submit(t2);
Future<Boolean> r3 = es.submit(t3);
//获取结果(call方法的返回值)
boolean rs1 = r1.get();
boolean rs2 = r2.get();
boolean rs3 = r3.get();
//关闭服务
es.shutdown();
}
private static class CallableTest implements Callable<Boolean>{
private int ticketNums = 10;
//实现call()方法,需要抛出异常
@Override
public Boolean call() throws Exception {
while (true){
if (ticketNums<=0){
break;
}
Thread.sleep(200);
System.out.println(Thread.currentThread().getName()+"抢到票"+ticketNums--);
}
return true;
}
}
线程状态
停止线程
不推荐使用JDK过时的方法(stop、destroy),最好是设置一个标志位,让线程自己停下来:
private static class testThread extends Thread{
private boolean flag = true;
@Override
public void run() {
int i = 0;
while (flag){
System.out.println("线程运行中"+i++);
}
}
public void stopThread(){
flag = false;
}
public static void main(String[] args) {
testThread tt = new testThread();
tt.start();
for (int i = 0; i < 1000000; i++) {
if (i==900000){
System.out.println("停止线程");
tt.stopThread();
}
}
}
}
线程休眠sleep方法
sleep(时间)指定当前线程阻塞的毫秒数;
sleep存在异常InterruptedException;(要处理异常)
sleep时间达到后线程进入就绪状态;
sleep可以模拟网络延时,倒计时等,放大问题的发生性。
每一个对象都有一个锁,sleep不会释放锁;
线程礼让yield方法
礼让线程,让当前正在执行的线程暂停,但不阻塞,线程会从运行状态转为就绪状态,让cpu重新调度,礼让不一定成功!看CPU心情
线程强制执行join方法
Join合并线程,待此线程执行完成后,再执行其他线程,其他线程会阻塞。
可以想象成插队,而且当前运行的线程都会停下来等待这个插队的线程跑完。
private static class threadTest implements Runnable{
//实现run()方法,编写线程执行体
@Override
public void run(){
for (int i = 0; i < 5; i++) {
System.out.println("VIP霸道插队"+i);
}
}
public static void main(String[] args) throws InterruptedException {
//创建线程对象,调用start()方法启动线程
threadTest tt = new threadTest();
Thread thread = new Thread(tt,"时钟1");
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程"+i);
if(i==5){
thread.join();
}
}
}
}
观测线程状态
Thread中有个State类定义了几个常量表示线程状态。
线程可以处于以下状态之一:
- NEW :尚未启动的线程处于此状态,new以后的就绪状态。
- RUNNABLE :在Java虚拟机中执行的线程处于此状态,运行状态。
- BLOCKED:被阻塞等待监视器锁定的线程处于此状态,阻塞状态。
- WAITING :正在等待另一个线程执行特定动作的线程处于此状态,阻塞状态
- TIMED_WAITING :正在等待另一个线程执行动作达到指定等待时间的线程处于此状态,阻塞状态。
- TERMINATED:已退出的线程处于此状态,线程一旦死亡就不能再次启动,得重新new。
观测方法:用getState()方法
线程优先级
Thread中定义了3个表示优先级的常量:
Thread.MIN_PRIORITY:最小优先级1
Thread.MAX_PRIORITY:最大优先级10
Thread.NORM_PRIORITY:默认优先级5
也可以设置1-10之间的数字来设置优先级,优先级低只是意味着先获得调度的概率低,并不是优先级低就一定被排在优先级高的后面执行,这都是看CPU的调度。
设置方法:setPriority方法、getPriority方法
Runnable runnable = ()->{
System.out.println(Thread.currentThread().getName()+"---->"+Thread.currentThread().getPriority());
};
System.out.println(Thread.currentThread().getName()+"---->"+Thread.currentThread().getPriority());
Thread t1 = new Thread(runnable,"t1");
Thread t2 = new Thread(runnable,"t2");
Thread t3 = new Thread(runnable,"t3");
Thread t4 = new Thread(runnable,"t4");
Thread t5 = new Thread(runnable,"t5");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(2);
t3.setPriority(Thread.MAX_PRIORITY);
t4.setPriority(Thread.NORM_PRIORITY);
t5.setPriority(7);
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
守护线程
线程分为用户线程和守护线程
虚拟机必须确保用户线程执行完毕,但是虚拟机不用等待守护线程执行完毕
如,后台记录操作日志,监控内存,垃圾回收等待
默认创建的都是用户线程。
如果要设为守护线程:setDaemon(true);//默认为false,默认为用户线程
public static void main(String [] args) {
Runnable people = ()->{
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"---->年龄:"+i);
}
System.out.println(Thread.currentThread().getName()+"---->寿终正寝");
};
Runnable god =()->{
while (true){
System.out.println("上帝保佑你");
}
};
Thread god1 = new Thread(god,"上帝守护线程");
god1.setDaemon(true);//默认为false,默认为用户线程
god1.start();
Thread t1 = new Thread(people,"人1");
Thread t2 = new Thread(people,"人2");
t1.start();
t2.start();
}
线程同步
并发:同一个对象被多个线程同时操作。
这是之前开启多线程的一个例子:
private static class buyTicket implements Runnable{
private int ticketNums = 10;
@Override
public void run() {
while(true) {
if (ticketNums<=0){
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到票"+ticketNums--);
}
}
public static void main(String[] args) {
buyTicket buyticket = new buyTicket();
new Thread(buyticket,"黄牛1").start();
new Thread(buyticket,"黄牛2").start();
new Thread(buyticket,"黄牛3").start();
}
}
可以看到黄牛1、2、3这三个线程同时对buyTicket对象的ticketNums进行操作了,每个线程在自己的工作内存交互,他们都同时把ticketNums拿出复制到自己的内存中操作了,这是线程不安全的。
处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。
除了排队,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。(要求线程排队操作以外还要上锁保证等待中的线程没有跑进来)
为了安全牺牲了一些性能,存在以下问题:
一个线程持有锁会导致其他所有需要此锁的线程挂起;
在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。
同步方法和同步块synchronized
synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
同步方法,锁的是this,同步方法就是在方法上加一个synchronized关键字。
同步块:synchronized(obj){}
Obj称之为同步监视器,Obj可以是任何对象,但是推荐使用共享资源作为同步监视器,同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class。
同步监视器的执行过程
1.第一个线程访问,锁定同步监视器,执行其中代码.
2.第二个线程访问,发现同步监视器被锁定,无法访问.
3.第一个线程访问完毕,解锁同步监视器.
4.第二个线程访问,发现同步监视器没有锁,然后锁定并访问
尽量减少同步块或同步方法里面复杂的代码。
修改成线程安全的代码:
private static class buyTicket implements Runnable{
private int ticketNums = 10;
@Override
public synchronized void run() {//加上synchronized关键字变成同步方法
while(true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ticketNums<=0){
break;
}
System.out.println(Thread.currentThread().getName()+"抢到票"+ticketNums--);
}
}
public static void main(String[] args) {
buyTicket buyticket = new buyTicket();
new Thread(buyticket,"黄牛1").start();
new Thread(buyticket,"黄牛2").start();
new Thread(buyticket,"黄牛3").start();
}
}
如果这样子改,先运行的线程买完全部的票以后才轮得到下一个线程。所以改成同步块,3个线程都可以进入run方法启动线程,只有在买票的时候需要排队:
private static class buyTicket implements Runnable{
private int ticketNums = 10;
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this){
if (ticketNums<=0){
break;
}
System.out.println(Thread.currentThread().getName()+"抢到票"+ticketNums--);
}
}
}
public static void main(String[] args) {
buyTicket buyticket = new buyTicket();
new Thread(buyticket,"黄牛1").start();
new Thread(buyticket,"黄牛2").start();
new Thread(buyticket,"黄牛3").start();
}
}
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。
某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题:
上图的例子,一个线程口红的锁还没有释放,就想去拿镜子的锁,而另一个线程中拿着镜子的锁没有释放,想去拿口红的锁。两个线程都在等待对方手中的锁,程序就卡在那里了。
解决办法:
产生死锁的四个必要条件:
1.互斥条件:一个资源每次只能被一个进程使用。
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3.不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。
4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
只要想办法破除其中一个条件,就可以避免死锁。
Lock锁
JDK5出现的特性,Lock是一个接口,提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
Lock拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
Lock和synchronized的区别:
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放
Lock只有代码块锁、
synchronized有代码块锁和方法锁
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
Lock>同步代码块(已经进入了方法体,分配了相应资源)>同步方法(在方法体之外)
Lock使用方法
private static class buyTicket implements Runnable{
private int ticketNums = 10;
private ReentrantLock lock = new ReentrantLock();//创建锁对象
@Override
public void run() {
while (true){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
lock.lock();//加锁
if (ticketNums<=0){
break;
}
System.out.println(Thread.currentThread().getName()+"抢到票"+ticketNums--);
}finally {
lock.unlock();//释放锁,一定记得释放锁,否则线程不结束,其他线程进不来。
}
}
}
public static void main(String[] args) {
buyTicket buyticket = new buyTicket();
new Thread(buyticket,"黄牛1").start();
new Thread(buyticket,"黄牛2").start();
new Thread(buyticket,"黄牛3").start();
}
}
线程协作
生产者消费者问题
生产者和消费者在同一时间段内共用同一个存储空间,生产者向空间里存放数据,而消费者取用数据,如果不加以协调可能会出现以下情况:
存储空间已满,而生产者占用着它,消费者等着生产者让出空间从而去除产品,生产者等着消费者消费产品,从而向空间中添加产品。互相等待,从而发生死锁。
解决生产者消费者问题,可以用管程法
管程法
管程法就是利用缓冲区去解决,生产者负责在缓冲区里放数据,消费者负责在缓冲区里取数据,缓冲区负责管理,当缓冲区容量满了就让生产者等待并且释放锁,唤醒消费者过来取数据,当缓冲区空了就让消费者等待并且释放锁,唤醒生产者过来放数据。
public static void main(String [] args) {
SynContainer synContainer = new SynContainer();
new Consumer(synContainer).start();
new Productor(synContainer).start();
}
//消费者
static class Consumer extends Thread{
private SynContainer synContainer;
public Consumer(SynContainer n){
this.synContainer = n;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
Production production = synContainer.Pop();
System.out.println("消费第"+production.ID+"产品");
}
}
}
//生产者
static class Productor extends Thread{
private SynContainer synContainer;
public Productor(SynContainer n){
this.synContainer = n;
}
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
synContainer.Push(new Production(i));
System.out.println("生产第"+i+"个产品");
}
}
}
//产品缓冲区
static class SynContainer{
private Production[] container = new Production[10];
int count = 0;
public synchronized Production Pop() {
if(count==0){//空了
try{
this.wait();
}catch (InterruptedException e){ }
}
count--;
Production production = container[count];
this.notifyAll();
return production;
}
public synchronized void Push(Production production) {
if (count == 9){//满了
try {
this.wait();
}catch (InterruptedException e){ }
}
container[count] = production;
count++;
this.notifyAll();
}
}
//产品
static class Production{
public int ID;
Production(int id){
this.ID = id;
}
}
信号灯法
信号灯法就是设置一个标志位来解决,生产了以后取反标志位,然后唤醒消费者来消费,消费者消费了以后又取反标志位,然后唤醒生产者生产。实际上就是一个缓冲区容量为1的管程法。
线程池
经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。
好处:
提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
public static void main(String [] args) {
ExecutorService ser = Executors.newFixedThreadPool(3);
Runnable r = ()->{
System.out.println(Thread.currentThread().getName());
};
ser.execute(r);//submit也可以
ser.execute(r);
ser.execute(r);
ser.shutdown();
}
参考:https://www.bilibili.com/video/BV1V4411p7EF Java多线程详解 讲得很清楚,非常感谢up:遇见狂神说
更多推荐
所有评论(0)