学习Java多线程看这一篇就够了


一、线程与进程

在没有使用多线程的程序中,程序只有一个主线程我们的程序是根据代码自上而下的。
而在多线程程序中,其他的线程与主线程同时运行。
如图:请添加图片描述
接下来我们介绍相关概念
说起进程,就不得不说程序

1、程序

程序是指令和数据的有序集合,其本身没有任何运行含义,是一个静态的概念

2、进程

而进程是一个动态的概念,每个运行中的程序就是一个进程,是系统进行资源分配和调度的一个独立单元

3、线程

通常在一个进程中可以包含若干线程,当然一个进程至少有一个线程,线程是CPU调度和执行的单位

进程是线程的容器,一个进程至少有一个线程。

每个线程都有各自的线程栈,自己的寄存器环境。

4、注意

①进程A与进程B资源不共享
②在java中:线程A与线程B,堆内存与方法区共享,线程的栈独立存在,一个线程一个栈。
③java中之所以有多线程,是为了提高程序的处理效率
④当main方法结束后,其他线程可能依然在运行
在这里插入图片描述

5、多线程编程的优势:

①进程之间不能共享内存,但线程之间共享内存非常容易。
②系统创建进程时需要为该进程重新分配系统资源,但创建线程的代价小得多,因此使用多线程来实现多任务并发比多进程效率高。
③Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。

二、多线程的实现

1、继承Thread类

继承Thread类,重写run()方法,调用start开启线程

//继承Thread类的方式实现多线程
public class Thread1 extends Thread{
	public void run() {
		for(int i=0;i<20;i++) {
			System.out.println("World"+i);
		}
		
	}
	
	public static void main(String[] args) {
		new Thread1().start();
		for(int i=0;i<100;i++) {
			System.out.println("Hello"+i);
		}
	}
}

注:线程开启不一定立即执行,由CPU调度执行

2、实现Runnable接口

定义类实现Runnable接口,实现run()方法编写线程执行体,创建线程对象,调用start()方法启动线程

public class TestThread2 implements Runnable{
    @Override
    public void run() {
        for(int i=0;i<20;i++){
            System.out.println("World==="+i);
        }
    }

    public static void main(String[] args) {
        //创建runnable接口的实现对象
        TestThread2 testThread2=new TestThread2();

        //创建线程对象,通过线程对象来开启我们的线程,代理(静态代理)
        new Thread(testThread2).start();
        for(int i=0;i<100;i++){
            System.out.println("Hello==="+i);
        }
    }
}

推荐使用Runnable对象,避免java单继承的局限性,方便同一个对象被多个线程使用。

3、实现Callable接口(了解即可)

public class TestCallable implements Callable {
    @Override
    public Boolean call() throws Exception {
        for(int i=0;i<20;i++){
            System.out.println("World "+i);
        }
        return true;
    }

    public static void main(String[] args) {
        TestCallable testCallable=new TestCallable();

        //创建执行服务
        ExecutorService ser= Executors.newFixedThreadPool(1);

        //提交执行
        Future<Boolean> r1=ser.submit(testCallable);

        //主线程执行
        for(int i=0;i<100;i++){
            System.out.println("Hello "+i);
        }

        //获取结果
        try {
            boolean rs1=r1.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        //关闭服务
        ser.shutdown();
    }
}

三、线程执行状态与方法

1、Thread.sleep(long millis)线程休眠

  • 每个线程都有一把锁,sleep不会释放锁

当前线程进入阻塞,持续millis毫秒
模拟倒计时:

//模拟倒计时
//打印当前系统时间
public class TestSleep {
    public static void main(String[] args) {
        Date startTime=new Date(System.currentTimeMillis());
        for(int i=10;i>=0;i--){
            try {
                Thread.sleep(1000);//主线程休眠1秒
                System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
                startTime=new Date(System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

2、线程停止

  • 建议使用次数,避免死循环
  • 建议使用标志位
  • 不建议使用JDK废弃的方法如stop、destory等

设置标志位使线程停止:

public class TestStop implements Runnable{
    //设置一个标志位
    private boolean flag=true;
    @Override
    public void run() {
        int i=0;
        while(flag){
            System.out.println("run......thread======"+i++);
        }
    }


    public void stop(){
        this.flag=false;
    }

    public static void main(String[] args) {
        TestStop testStop=new TestStop();
        new Thread(testStop).start();
        for(int i=0;i<1000;i++){
            System.out.println("main===="+i);
            if(i==900){
                testStop.stop();
                System.out.println("线程停止");
            }
        }
    }
}

3、Thread.yield()线程礼让

让当前正在执行的线程暂停,但不阻塞
将线程从运行态转为就绪态
让CPU重新调度,礼让不一定能成功(看概率)

public class TestYield{
    public static void main(String[] args) {
        MyYield myYield1=new MyYield();

        new Thread(myYield1,"a").start();

        new Thread(myYield1,"b").start();
    }

}
class MyYield implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始===");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"线程结束===");
    }
}

礼让失败:

a线程开始===
a线程结束===
b线程开始===
b线程结束===

礼让成功:

a线程开始===
b线程开始===
a线程结束===
b线程结束===

4、Thread.join()线程强制执行

join合并线程,待此线程完成后再执行其他线程,即将其他线程阻塞,可以想象成插队

public class TestJoin implements Runnable{
    @Override
    public void run() {
        for (int i=0;i<1000;i++){
            System.out.println("VIP==="+i);
        }
    }


    public static void main(String[] args) {
        TestJoin testJoin=new TestJoin();
        Thread thread=new Thread(testJoin);
        thread.start();

        //主线程
        for(int i=0;i<500;i++){
            if(i==100){
                try {
                    thread.join();  //插队(强制执行,容易产生阻塞)
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("主线程==="+i);
        }
    }
}

当主线程循环达到100次时,会让thread线程先运行完。

5、守护线程

Java线程分为用户线程和守护线程。
守护线程是程序运行的时候在后台提供一种通用服务的线程。所有用户线程停止,进程会停掉所有守护线程,退出程序。
Java中把线程设置为守护线程的方法: setDaemon(true) 方法。

public class TestDaemon implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println("守护线程运行===");
        }
    }

    public static void main(String[] args) {
        TestDaemon testDaemon=new TestDaemon();
        Thread thread=new Thread(testDaemon);
        thread.setDaemon(true); //设置为守护线程
        thread.start();

        for(int i=0;i<10;i++){
            System.out.println("主线程(用户线程)运行==="+i);
        }
        System.out.println("主线程退出===");
    }
}

从打印结果可以看出,在主线程结束之后,守护线程等jvm退出之后才停止运行
注意:

  • setDaemon(true) 必须在 start()之前设置,否则会抛出IllegalThreadStateException异常,该线程仍默认为用户线程,继续执行
  • 守护线程创建的线程也是守护线程
  • 守护线程不应该访问、写入持久化资源,如文件、数据库,因为它会在任何时间被停止,导致资源未释放、数据写入中断等问题

四、线程同步

在介绍线程同步之前我们先来讲一下什么是线程不安全
我们模拟了一个线程不安全的买票过程

//不安全的买票
//每个线程都在自己的工作内存交互,内存控制不当会造成数据不一致
public class UnsafeBuyTickets {

    public static void main(String[] args) {
        Station station=new Station();
        new Thread(station,"小明").start();
        new Thread(station,"小红").start();
        new Thread(station,"ben").start();
    }
}

class Station implements Runnable{

    //票
    static int tickets=10;

    //标志位
    static boolean flag=true;
    @Override
    public void run() {
        while(flag){
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void buy() throws InterruptedException {
        if(tickets<=0){
            flag=false;
            return;
        }

        
        //买票
        System.out.println(Thread.currentThread().getName()+"买到了"+tickets--);
        //模拟延时
        Thread.sleep(100);
    }
}
小明买到了10
ben买到了9
小红买到了8
小明买到了7
ben买到了6
小红买到了7
ben买到了5
小红买到了5
小明买到了4
小红买到了3
ben买到了2
小明买到了3
ben买到了1
小红买到了0
小明买到了-1

最后的结果出现了负数,明显错了。
出现该错误的原因:由于统一进程的多个线程共享一块存储空间,在带来方便的同时,也带来了访问冲突的问题,如两个线程同时访问该资源

1、synchronized关键字

  • 由于我们可以通过private关键字来保证数据对象只能被方法访问,所以我们要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法synchronized方法synchronized块
  • synchronized方法控制对“对象”的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到方法返回才释放。

我们将上述不安全的买票过程改写一下,加上synchronized关键字:

①同步方法

//安全的买票
public class SafeBuyTickets {
    public static void main(String[] args) {
        Station1 station1=new Station1();
        new Thread(station1,"小明").start();
        new Thread(station1,"小红").start();
        new Thread(station1,"ben").start();
    }


}

class Station1 implements Runnable{

    //票
    static int tickets=10;

    //标志位
    static boolean flag=true;
    @Override
    public void run() {
        while(flag){
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    //同步锁,锁的是this
    public synchronized void buy() throws InterruptedException {
        if(tickets<=0){
            flag=false;
            return;
        }

        //模拟延时
        Thread.sleep(15);
        //买票
        System.out.println(Thread.currentThread().getName()+"买到了"+tickets--);
    }
}

结果为:

小明买到了10
小红买到了9
小红买到了8
小红买到了7
小红买到了6
小红买到了5
小红买到了4
小红买到了3
ben买到了2
ben买到了1

锁机制也存在一下问题

  • 一个线程持有锁会导致所有需要此锁的线程挂起
  • 在多线程竞争下,加锁,释放锁会产生比较多的上下文切换调度延时,引起性能问题
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

②同步块

我们知道ArrayList是线程不安全的集合

public class UnsafeArrayList {
    public static void main(String[] args) {
        ArrayList<String> list=new ArrayList<>();
        for(int i=0;i<10000;i++){
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

在list里的对象数本应该是10000个,但出现了线程不安全

9999

此时我们用synchronized对list加锁,即把list添加对象的语句用同步块包住

public class SafeArrayList {
    public static void main(String[] args) {
        ArrayList<String> list=new ArrayList<>();
        for (int i=0;i<10000;i++){
            new Thread(() -> {
                synchronized (list) {
                    list.add(Thread.currentThread().getName());
                }
            }).start();
            }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

list大小为10000

10000

2、Lock(锁)

  • 从JDK5.0开始,Java提供了强大的线程同步机制–通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当
  • java.util.concurrent.locks.Lock接口时控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个现成对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁,释放锁。

用Lock实现同步锁买票过程

public class TestLock {
    public static void main(String[] args) {
        TestLock2 testLock2=new TestLock2();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
    }

}

class TestLock2 implements Runnable{

    int ticketNum=10;

    //定义一把锁
    ReentrantLock lock=new ReentrantLock();


    @Override
    public void run() {

        while(true){
            try{
                lock.lock();    //加锁
                if(ticketNum>0){
                    try {
                        Thread.sleep(1000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println(ticketNum--);
                }else{
                    break;
                }
            }finally {
                 lock.unlock();     //释放锁
            }
        }
    }
}

3、synchronized与Lock对比

  • Lock是显式锁(手动开启和关闭),synchronized是隐式锁,出了作用域自动释放。
  • Lock是有代码块锁,synchonrized有代码块锁和方法锁。
  • 使用Lock锁,JVM将花费较少的时间来调度,性能更好。并且有更多的扩展性
  • 优先使用顺序:Lock>同步代码块>同步方法

五、线程通信

1、生产者消费者问题

在讲该部分之前我们需要先介绍一个经典线程通信问题:“生产者消费者问题”。

生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
.
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。

请添加图片描述

  • 假设仓库只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中的产品取走消费
  • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
  • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品位置

分析:生产者消费者共享一个资源,并且他们之间相互依赖,互为条件

  • 对于生产者,没有生产产品之前,要通知消费者等待;在生产了产品之后又要通知消费者消费
  • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品

2、解决线程通信问题的方法

请添加图片描述
注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用

3、管程法解决生产者消费者问题

并发协作模型—>管程法
生产者生产的产品放入缓冲区,消费者从缓冲区拿出产品

//生产者消费者模型,管程法
public class PcTest {

    public static void main(String[] args) {
        SynContainer container=new SynContainer();

        Productor productor=new Productor(container);

        Comsumer comsumer=new Comsumer(container);

        productor.start();

        comsumer.start();
    }
}

//生产者
class Productor extends Thread{

    private SynContainer container;

    public Productor(SynContainer container){
        this.container=container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            container.push(new Product(i));
            System.out.println("生产者生产了第"+i+"个产品===");
        }
    }
}

//消费者
class Comsumer extends Thread{
    private SynContainer container;

    public Comsumer(SynContainer container){
        this.container=container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {

            System.out.println("消费者消费了第"+container.pop().id+"件产品--->");
        }
    }
}

//产品
class Product{
    public int id;
    public Product(int id){
        this.id=id;
    }
}

//安全的缓冲区
class SynContainer{
    Product[] products=new Product[10];

    //容器内产品的数量
    static int count=0;

    //生产者放入产品
    public synchronized void push(Product product){
        //如果容器满了,等待消费者消费
        while(count>=products.length) {
            //通知消费者消费,生产者等待
            try {
                System.out.println("========容器满了========");
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //如果没有满,通知消费者消费
        products[count]=product;
        count++;
        this.notifyAll();
    }

    //消费者消费产品
    public synchronized Product pop(){
        //判断能否消费
        while(count==0){
            //等待生产者生产
            try{
                System.out.println("========容器空了========");
                this.wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }

        //可以消费
        count--;

        Product product=products[count];
        this.notifyAll();
        return product;
        }
   }

六、线程池

  • JDK起提供了线程池相关APIExecutorServiceExecutors
  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
  • void execute(Runnable command):用来还行Runnable
//测试线程池
public class TestPool {

    public static void main(String[] args) {

        //1、创建线程池
        //newFixedThreadPool,参数为线程池大小
        ExecutorService service= Executors.newFixedThreadPool(10);

        //执行
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        //2、关闭链接
        service.shutdown();
    }
}

class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

结果

pool-1-thread-1
pool-1-thread-4
pool-1-thread-3
pool-1-thread-2
pool-1-thread-6
pool-1-thread-5
pool-1-thread-7
pool-1-thread-8
pool-1-thread-9
pool-1-thread-9
pool-1-thread-10
pool-1-thread-9

无论执行多少个execute,都只会拿线程池里的线程

参考博文:狂神说java–多线程笔记(及源码)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值