【JAVA SE】这篇万字文章带你弄懂JAVA中的多线程

2年前 (2022) 程序员胖胖胖虎阿
270 0 0

大家好呀,我是crisp制药呀💊,今天来给大家讲解JAVA中多线程的相关的知识,万字长文希望大家可以收藏起来慢慢看,作为初学者我也是写来方便日后的复习,制药和大家一起学习一起加油!😀
【JAVA SE】这篇万字文章带你弄懂JAVA中的多线程

如果喜欢的话,欢迎大家关注,点赞,评论,收藏!
🌟本人博客的首页:crisp制药
😺本人是一个JAVA初学者,分享一下自己的所学,如果有错误,希望大家能告知,谢谢!

【JAVA SE】这篇万字文章带你弄懂JAVA中的多线程

文章目录

      • 🔮1、程序、进程、线程
      • 📚线程的创建和使用
        • 📎Tips:
      • 💠线程的同步
        • 💒方式一:同步代码块
        • 💐方式二:同步方法
        • 🔗同步方法总结
          • 线程安全的单例模式:懒汉式
      • 🔏死锁
      • 🔒Lock(锁)
        • 💎synchronized和lock的不同:
      • 💍解决线程安全问题
      • 💌线程的通信
        • 💻 sleep()和wait()的异同
        • 📆实现Callable接口
        • 📌Future接口
      • 📏使用线程池
      • 📐总结

🔮1、程序、进程、线程

程序:一段静态的代码,静态对象。

进程:正在运行的一个程序,是一个动态的过程,有生命周期。进程作为资源分配的单位,系统在运行时会为每个进程分配内存区域。

线程:进程可以进一步细化为线程,是一个程序内部的一条执行路劲,同一个进程可以支持多个线程就叫多线程
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器,每个线程各自有一套虚拟机栈和程序计数器
>每个进程各有一份方法区和堆,多个线程共享相同的内存单元,内存地址空间,

🔯并行和并发
并行——多个CPU同时执行多个任务

并发——一个CPU同时执行多个任务
🔯多线程程序的优点:
1.提高程序的响应。对图形化界面更有意义,可增强用户体验
2.提高计算机系统CPU的利用率
3.改善程序结构,进程分为线程独立运行,有利于理解和修改

📚线程的创建和使用

方式一:继承于Thread类
1、创建Thread类的子类
2、重写Thread类的方法
3、创建Thread类的子类对象
4、通过此对象调用start()

class MyThread extends Thread{
    @Override
    public void run(){
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }
}
public static void main(String[] args) {
    MyThread t=new MyThread();//先写个new MyThread();然后alt+enter 快速创建对象
    t.start();//①启动当前线程 ②调用当前线程的run()
    //匿名子类
    new Thread(){
        @Override
        public void run(){
        }
    }.start();
}
//执行的先后顺序并不会按照写的先后顺序,两个线程是并行的

📎Tips:

不能通过run()的方式执行线程,用run()只是普通的调用方法,直接调用run只是对象调方法还是单线程,一个对象只能调用一次start();
如果还要再启动一个线程,就再新造一个MyThread对象,再start()

💫Thread里的常用方法:
start():①启动当前线程 ②调用当前线程的run()
run():通常需要重写此方法, currentThread():静态方法,返回执行当前代码的线程 getName():获取当前线程的名字。
setName():设置当前线程的名字 yield():释放当前CPU的执行权。
join():在线程A中调用B的该方法,线程A进入阻塞状态直到线程B执行完之后线程A才继续执行。
sleep():让当前线程强制阻塞指定的时间,SLEEP()括号中放数字,单位是毫秒,控制线程的时间,sleep是个静态方法,sleep自带异常:InterruptedException
boolean isAlive() 判断线程是否存活

💡线程的优先级:
MAX——10,MIN——1,NORM——5(默认的优先级)
涉及的方法:getPriority()——返回线程的优先值,setPriority()——改变线程的优先级,高优先级的线程不一定就先执行,只是大概率先执行。

💟方式二:实现Runnable接口
创建一个实现Runnable接口的类
实现类实现Runnable中的抽象方法:run()方法
创建实现类的对象 将对象作为参数传递到Thread类的构造器中
创建Thread类的对象 通过此对象调用start()方法

class MyRun implements Runnable{//创建一个实现Runnable接口的类
    @Override
    public void run(){//实现类实现Runnable中的抽象方法:run()方法
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }
}
public static void main(String[] args) {
    MyRun r=new MyRun();//创建实现类的对象
    Thread t=new Thread(r);//将对象作为参数传递到Thread类的构造器中,创建Thread类的对象
    t.start();//通过此对象调用start()方法
    
    //再启动一个线程
     Thread t1=new Thread(r);
     t1.start();
}

💠线程的同步

线程安全问题:

  • 多个线程执行的不确定性引起执行结果的不稳定性
  • 多个线程对数据的共享会造成操作的不完整性,会破坏数据
    解决:当一个线程A在操作共享数据的时候其他线程不能参与,直到线程A操作完毕,也就是同步机制。

💒方式一:同步代码块

synchronized(同步监视器){
    //需要被同步的代码
}

1 操作共享数据的代码即为需要被同步的代码
2 共享数据:多个线程共同操作的变量
3 同步监视器,俗称锁。任何类的对象都可以充当锁,多个线程必须共用同一把锁。
4 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器;而在继承Thread类创建多线程的方式中,可以考虑使用当前类

//同步代码块+实现Thread类
class MyThread extends Thread{
    private static int ticket=100;
    private static Object obj=new Object();
    @Override
    public void run(){
        synchronized (MyThread.class){//类只会加载一次
        //synchronized (this){//实现Runnable接口创建多线程的方式用this
        //synchronized(obj){//造了三个对象,有三把锁,所以不能保证线程安全
            for (tikect>0) {
                System.out.println(getName()+"买票,票号为:"+ticket);
                ticket--;
        	}
        }
    }
}
public static void main(String[] args) {
    MyThread t1=new MyThread();
    MyThread t2=new MyThread();
    MyThread t3=new MyThread();
    t1.start();
    t2.start();
    t3.start();
}

💐方式二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,不妨将此方法声明为同步的

//同步方法+实现Runnable
class MyRun implements Runnable{
    private static int ticket=100;
    @Override
    public void run(){
        show();
    }
    public synchronized void show(){//这里的同步监视器就是this
        for (tikect>0) {
            System.out.println(getName()+"买票,票号为:"+ticket);
            ticket--;
        }
    }
}
public static void main(String[] args) {
    MyRun r=new MyRun();
    
    Thread t1=new Thread(r);
    Thread t2=new Thread(r);
    Thread t3=new Thread(r);
    t1.start();
    t2.start();
    t3.start();
}
//同步方法+实现Thread类
class MyThread extends Thread{
    private static int ticket=100;
    @Override
    public void run(){
        show();
    }
    public static synchronized void show(){//必须是静态的,这里的同步监视器是当前类
        for (tikect>0) {
            System.out.println(Thread.currentThread.getName()+"买票,票号为:"+ticket);
            ticket--;
        }
    }
}

🔗同步方法总结

同步代码块
继承Thread类:用当前类当作同步监视器(MyThread.class)
实现Runnable接口:用this当同步监视器
同步方法:
继承Thread类:直接用synchronized修饰方法,this当同步监视器
实现Runnable接口:需要用synchronized static修饰方法,当前类当同步监视器

线程安全的单例模式:懒汉式
class MyThread extends Thread{
    @Override
    public void run(){
        Bank.getInstance();
    }
}
class Bank{
    private Bank(){

    }
    private static Bank instance=null;
    //方法1:同步方法
    public static synchronized Bank getInstance(){//加上synchronized
        if (instance==null)instance=new Bank();
        return instance;
    }
    //方法2:同步代码块(效率稍差)
    public static Bank getInstance(){
        synchronized(Bank.class){
            if (instance==null)instance=new Bank();
            return instance;
        }
    }
    //效率更高
    public static Bank getInstance(){
        if (instance==null){
            synchronized(Bank.class){
                if (instance==null)instance=new Bank();
                return instance;
            }
        }
    }

}

🔏死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃,造成了线程的死锁,没有异常没有提示,但所有线程都在阻塞状态。

解决:①专门的算法、原则;②尽量减少同步资源的定义;③尽量避免嵌套同步

🔒Lock(锁)

从JDK5.0开始,JAVA提供了更强大的线程同步机制——通过显式定义同步锁来实现同步,同步锁使用Lock对象充当。java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。

class MyRun implements Runnable{
    private static int ticket=100;
    //1.实例化ReentrantLock
    private ReentrantLock lock=new ReentrantLock(true);
    @Override
    public void run(){
        try {
            //2.调用lock()方法
            lock.lock();
            while (ticket>0){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"票号为:"+ticket);
                ticket--;
            }
        }finally {
            //3.调用解锁的方法
            lock.unlock();
        }
    }
}
//如果是继承Thread方式,private static ReentrantLock lock=new ReentrantLock(true)要加static

💎synchronized和lock的不同:

Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放

Lock只有代码块锁,synchronized有代码块锁和方法锁

使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)

2优先使用顺序:

Lock >同步代码块(已经进入了方法体,分配了相应资源) >同步方法(在方法体之外)

💍解决线程安全问题

synchronized(分为同步代码块和同步方法)和锁

练习

class Account{
    private  double balance;

    public Account(double balance) {
        this.balance = balance;
    }
    public  synchronized  void deposit(double amt){
        if(amt>0){
            balance += amt;


            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
            }
            System.out.println(Thread.currentThread().getName()+"存钱成功余额为:"+balance);
        }
    }
}
class Customer extends Thread{
    private Account acct;

    public Customer(Account acct) {
        this.acct = acct;
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
               acct.deposit(1000);
        }
    }
}
public class AccountTest {
    public static void main(String[] args) {
        Account acct = new Account(0);


        Customer c1 = new Customer(acct);
        Customer c2 = new Customer(acct);

        c1.setName("甲");
        c2.setName("乙");
        c1.start();
        c2.start();
    }
}

💌线程的通信

class MyRun implements Runnable{
    private static int num=1;
    @Override
    public void run(){
        while (true){
            synchronized (this){
                notify();//*********
                if (num<=100){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"当前数字:"+num);
                    num++;
                    try {
                        wait();//*********
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

💖涉及的三个方法:

wait():强制当前线程等待,直到某个其他线程在同一个对象上调用notify()或notifyAll()

notify():唤醒wait中的一个线程,如果有多个线程被阻塞,则唤醒优先级高的那个

notifuAll():唤醒所有wait中的线程

说明:

三个方法必须使用在同步代码块或者同步方法中
三个方法的调用者必须是同步监视器,否则会出现IllegalMonitorStateException异常 三个方法声明在Object类中

💻 sleep()和wait()的异同

相同点:一旦执行方法,都可以使当前线程进入阻塞状态

不同点:

声明位置不同,sleep()声明在Thread类中,wait()声明在Object类中
调用的要求不同,sleep()可以在任何需要的场景下调用,wait()必须在同步代码块或者同步方法中
关于是否释放同步监视器,如果两个方法都使用在同步代码块或者同步方法中,wait()会释放同步监视器

💼经典例题:生产者/消费者问题

class Clerk{

    private  int productCount = 0;

    public synchronized void produceProduct() {
        if(productCount<20){
            productCount++;
            System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");
            notify();
        }else{
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public  synchronized void consumeProduct() {
        if(productCount > 0){
            System.out.println(Thread.currentThread().getName() + "开始消费第" + productCount + "个产品");
            productCount--;
        }else{
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}

class Producer  extends  Thread{

    private Clerk clerk;

    public  Producer(Clerk clerk){
        this.clerk=clerk;
    }

    @Override
    public void run() {
        System.out.println(getName()+":开始生产产品......");

        while(true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.produceProduct();
        }
    }
}
class Consumer extends  Thread{
    private Clerk clerk;

    public   Consumer (Clerk clerk){
        this.clerk=clerk;
    }

    @Override
    public void run() {
        System.out.println(getName()+":开始生产产品......");

        while(true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consumeProduct();
        }

    }
}
public class ProductTest {
    public static void main(String[] args) {
        Clerk clerk =new Clerk();

         Producer p1 = new Producer(clerk);
         p1.setName("生产者1");

        Consumer c1 = new Consumer(clerk);
        c1.setName("消费者1");

        p1.start();
        c1.start();

    }
}

生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产:如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

📎这里可能出现两个问题:

生产者比消费者快时,消费者会漏掉一些数据没有取到。

消费者比生产者快时,消费者会取相同的数据。

我理解的线程安全问题:两个线程分别生产和消费,但是操作了共同数据那就是产品,所以会出现线程安全问题,就是必须规定一段时间内只能生产或者只能消费

📆实现Callable接口

1、与使用Runnable相比, Callable功能更强大些

相比run方法, 可以有返回值
方法可以抛出异常
支持泛型的返回值
需要借助FutureTask类, 比如获取返回结果

📌Future接口

可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
FutrueTask是Futrue接口的唯一的实现类
FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

//1.创建一个实现Callable类的实现类
class MyCall implements Callable{
    //2.实现call方法,将此线程需要执行的操作声明在call方法中
    @Override
    public Object call() throws Exception {
        return 1;
    }
}
public static void main(String[] args) {
    //3.创建Callable实现类的对象
    MyCall myCall=new MyCall();
    //4.将Callable实现类的对象传递到FutureTask构造器中,创建FutureTask的对象
    FutureTask<Integer> myTask=new FutureTask<Integer>(myCall);//可以使用泛型
    //5.将FutureTask的对象传递到Thread构造器中并调用start方法
    new Thread(myTask).start();//启动线程还是要用start()方法
   //1.创建一个实现Callable类的实现类
class NumThread implements Callable{
    //2.实现call方法,将此线程需要执行的操作声明在call方法中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <=100 ; i++) {
            if(i%2==0){
                System.out.println(i);
                sum+=i;
            }
        }
        return sum;
    }
}
public class ThreadNew {
    public static void main(String[] args) {
        //3.创建Callable实现类的对象
        NumThread numThread = new NumThread();
        //4.将Callable实现类的对象传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5.将FutureTask的对象传递到Thread构造器中并调用start方法
        new Thread(futureTask).start();
        try {
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值.
            Object sum = futureTask.get();
            System.out.println(sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

}
    try {
        myTask.get();//可以得到返回值1
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

📏使用线程池

开发中都是用线程池,好处:
减少创建新线程的时间,提高响应速度
重复利用线程池中的线程降低资源消耗 便于线程管理

注意:
1、JDK 5.0起提供了线程池相关API: ExecutorService 和Executors
2、ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
Future submit(Callable task); 执行任务,有返回值,一般又来执行Callable
void shutdown():关闭线程池
3、Executors: 工具类、线程池的工厂类,用于创建并返回不同类型的线程池 ➢Exefutors newCachedThreadPool():创建一个可根据需要创建新线程的线程池 ➢Executors .newFixedThreadPool(n);创建一个可重用固定线程数的线程池

Executors. newSingleThreadExecutor): 创建一个只有一个线程的线程池
Executors newScheduledThreadPool(n): 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行

public static void main(String[] args) {
    //1.提供指定数量的线程池
    ExecutorService service= Executors.newFixedThreadPool(10);
    //2.执行指定的线程的操作,需要提供实现Runnable接口或者Callable接口实现类的对象
    service.execute(new NumberThread());//适用于Runnable
    service.submit(new MyCall());//适用于Callable
    service.shutdown();//3.关闭线程池
}
class NumberThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i%2==0)System.out.println(i);
        }
    }
}
class MyCall implements Callable{
    @Override
    public Object call() throws Exception {
        return 1;
    }
}

📐总结

📢1、创建多线程有四种方式:

继承Thread类
实现Runnable接口
实现Callable接口
使用线程池
📫2、同步的方式:

同步代码块

继承Thread类:用当前类当作同步监视器(MyThread.class)

实现Runnable接口:用this当同步监视器

同步方法

继承Thread类:直接用synchronized修饰方法,this当同步监视器
实现Runnable接口:需要用synchronized static修饰方法,当前类当同步监视器 定义同步锁

🔔3、释放锁的操作

当前线程的同步方法、同步代码块执行结束。
当前线程在同步代码块、同步方法中遇到break、returm终止了该代码块、该方法的继续执行。
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
🔥4、不会释放锁的操作

线程执行同步代码块或同步方法时,程序调用Thread sleep()、 Thread.
yield()方法暂停当前线程的执行
线程执行同步代码块时,其他线程调用了该线程的suspend()方法(挂起)将该线程挂起,该线程不会释放锁(同步监视器)
ps:应尽量避免使用suspend()和resume()来控制线程

📒 今天学习了📚线程的创建和使用
💠线程的同步,🔗同步方法总结,线程安全的单例模式:懒汉式,🔏死锁,🔒Lock(锁),💎1synchronized和lock的不同:,💍解决线程安全问题,💌5、线程的通信,💻 sleep()和wait()的异同
📆实现Callable接口,📌2、Future接口,📏使用线程池
万字长文拜托各位大佬三连支持一下哦~
【JAVA SE】这篇万字文章带你弄懂JAVA中的多线程

相关文章

暂无评论

暂无评论...