17.多线程

news2024/11/15 13:35:23

多线程

程序、进程、线程的概念

程序:是指令和数据的有序集合,是一个静态的概念。比如,在电脑中,打开某个软件,就是启动程序。

进程:是执行程序的一次执行过程,是一个动态的概念,是系统资源的分配单位。

当我们运行一个程序后,这个程序的进程就会启动。将来可以在任务管理器中查看进程。

线程:一个进程中,可以包含若干个线程,至少包含一个线程。线程是CPU调度和执行的单位。

在同一个进程中,可以执行多个任务,每个任务就可以看成是一个线程。

比如:放音乐的时候,可以一边放音乐,一边下载其他音乐;

看电影的时候,播放器中的画面、声音、弹幕,都是独立的线程。

总结:

进程可以指运行中的程序,特点:动态性、独立性、并发性。

线程是进程内部的执行单位,它是程序中一个顺序控制流程。

多线程就是一个进程中,同时有多个线程,用于完成不同的工作。

Java中的线程

主线程、子线程

main()方法就是主线程的入口,其中执行的内容就是主线程内容,其他的子线程需要通过特殊的类,去创建在main()方法中执行,main()方法必须最后执行完毕,因为它要执行各种关闭操作。

package com.day17.thread1;

public class ThreadDemo {
    public static void main(String[] args) {
        //测试main()方法中的线程
        //返回当前main()方法中的线程对象
        Thread thread = Thread.currentThread();
        //返回当前线程名称
        System.out.println(thread.getName());

        //设置main线程的名字
        thread.setName("主线程");
        System.out.println(thread.getName());
    }
}

Java中线程的创建和启动

1、继承Thread类

构造方法

Thread()

分配一个新的 Thread对象。

Thread(Runnable target)

分配一个新的 Thread对象。

Thread(Runnable target, String name)

分配一个新的 Thread对象。

Thread(String name)

分配一个新的 Thread对象。

常用普通方法

static Thread currentThread()

返回对当前正在执行的线程对象的引用。

String getName()

返回此线程的名称。

void join()

等待这个线程死亡。

void run()

如果这个线程使用单独的Runnable运行对象构造,则调用该Runnable对象的run()方法;

否则,此方法不执行任何操作并返回。

void setName(String name)

将此线程的名称更改为等于参数 name 。

void setPriority(int newPriority)

更改此线程的优先级。

static void sleep(long millis)

使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。

void start()

导致此线程开始执行; Java虚拟机调用此线程的run方法。

static void yield()

对调度程序的一个暗示,即当前线程愿意产生当前使用的处理器。

继承Thread类创建多线程的步骤

1、继承Thread类

2、重写run方法,将来要其他线程要执行的逻辑内容,都放在run方法中

3、实例化子类对象后,调用start方法来启动线程,

调用start方法的时候,是在告诉JVM分配线程去调用run()方法

调用run()方法和start()方法的区别

run方法是继承了Thread类重写的一个方法,

当我们调用start()方法的时候,JVM会启动一个线程,并执行run()方法中的内容,

直接调用run()方法,会把run()当作main()线程下的一个普通方法执行,

不会在某个线程中执行,所以并不是多线程工作。

package com.day17.thread1;

public class ThreadDemo01 extends Thread{
    //继承Thread类创建多线程的步骤
    //1、继承Thread类
    //2、重写run方法,将来要其他线程要执行的逻辑内容,都放在run方法中
    //3、实例化子类对象后,调用start方法来启动线程
    //调用start方法的时候,是在告诉JVM分配线程去调用run()方法


    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            //获取当前线程名称
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }

    public static void main(String[] args) {
        //main方法既是程序的入口,也是一个主线程,也是子线程启动的入口
        ThreadDemo01 thread01 = new ThreadDemo01();
        thread01.setName("子线程1");
        thread01.start();//启动线程
        //直接调用run方法,JVM默认会将run方法作为普通方法交给main线程来执行
        // thread01.run();

//        ThreadDemo01 thread02 = new ThreadDemo01();
//        thread02.setName("子线程2");
//        thread02.start();

    }
}

2、实现Runnable接口

1、实现Runnable接口,重写run()方法

2、创建对象,把对象作为参数,传递到Thread类中中,创建Thread对象,创建出来的Thread对象才是真正的线程对象

3、通过线程对象,调用start()方法

package com.day17.thread2;

//1、实现Runnable接口,重写run()方法
//2、创建对象,把对象作为参数,传递到Thread类中中,创建
//Thread对象,创建出来的Thread对象才是真正的线程对象
//3、通过线程对象,调用start()方法
public class ThreadDemo implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+": "+i);
        }
    }

    public static void main(String[] args) {
        //先创建Runnable接口实现类对象
        ThreadDemo thread01 = new ThreadDemo();
        //        //再通过实现类对象,创建Thread对象
        //        Thread t1 = new Thread(thread01);
        //        //通过Thread对象调用start()方法
        //        t1.start();
        //        //将创建Runnable接口对象,作为参数直接传入Thread类的构造方法
        //        Thread t2 = new Thread(new ThreadDemo(), "子线程2");
        //        t2.start();

        Thread t1 = new Thread(thread01, "小红");
        Thread t2 = new Thread(thread01, "小明");
        t1.start();
        t2.start();

    }
}

使用匿名内部类实现Runnable接口,实现多线程效果

package com.day17.thread2;
//使用匿名内部类实现Runnable接口,实现多线程效果
public class ThreadDemo02 {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+": "+i);
                }
            }
        },"小明").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println(Thread.currentThread().getName()+": "+i);
                }
            }
        },"小红").start();

    }
}

lambda表达式的写法实现Runnable接口,完成多线程实现

是一种函数式编程

语法:()->{} ()中将来传递参数,{}写重写接口的方法

package com.day17.thread2;
//lambda表达式的写法实现Runnable接口,完成多线程实现
//是一种函数式编程
//语法:()->{}  ()中将来传递参数,{}写重写接口的方法
public class ThreadDemo03 {
    public static void main(String[] args) {
        new Thread(
                ()->{
                    for (int i = 0; i < 10; i++) {
                        System.out.println(Thread.currentThread().getName()+": "+i);

                    }
                }
                ,"小红").start();

        new Thread(
                ()->{
                    for (int i = 0; i < 10; i++) {
                        System.out.println(Thread.currentThread().getName() + ": " + i);
                    }
                }
                ,"小明").start();
    }
}

3、实现Callable接口

实现步骤:

1.实现Callable接口

2.以实现类为参数,创建FutureTask对象

3.将FutureTask对象作为参数,创建Thread对象

4.调用start方法

package com.day17.thread7;

import java.util.concurrent.Callable;

public class CallableDemo implements Callable {
    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName()+"->call方法被执行了!");
        return 10;
    }
}
package com.day17.thread7;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class TestCallable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建FutureTask对象
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new CallableDemo());
        new Thread(futureTask).start();

        //获取返回call方法的返回值,要通过FutureTask对象获取
        System.out.println(futureTask.get());
    }
}

4、使用Executors工具类创建线程池

package com.day17.thread8;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorsDemo extends Thread{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"正在执行...");
    }

    public static void main(String[] args) {
        //通过Executors工具类创建线程池

        //可变大小的线程池
        ExecutorService pool = Executors.newCachedThreadPool();
        //固定大小的线程池
//        ExecutorService pool = Executors.newFixedThreadPool(2);
        //单个的
//        ExecutorService pool = Executors.newSingleThreadExecutor();


        //创建线程对象
        ExecutorsDemo t1 = new ExecutorsDemo();
        ExecutorsDemo t2 = new ExecutorsDemo();
        ExecutorsDemo t3 = new ExecutorsDemo();
        ExecutorsDemo t4 = new ExecutorsDemo();

        //把线程对象放入池子中
        pool.execute(t1);
        pool.execute(t2);
        pool.execute(t3);
        pool.execute(t4);

        //关闭线程池
        pool.shutdown();
    }
}
package com.day17.thread8;

import java.util.Date;

public class ExecutorsDemo02 implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"开始执行:"+new Date());

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"结束执行:"+new Date());

    }
}
package com.day17.thread8;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExecutorDemo {
    public final static int CORE_POOL_SIZE = 5;
    public static final int MAX_POOL_SIZE = 10;
    public static final Long KEEP_ALIVE_TIME = 1L;
    //使用阿里巴巴推荐的ThreadPoolExecutor创建线程池
    public static void main(String[] args) {
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
            CORE_POOL_SIZE,
            MAX_POOL_SIZE,
            KEEP_ALIVE_TIME,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100)
        );
        for (int i = 0; i < 10; i++) {
            ExecutorsDemo02 demo02 = new ExecutorsDemo02();
            poolExecutor.execute(demo02);
        }
        //关闭线程池
        poolExecutor.shutdown();
    }
}

继承Thread类和实现Runnable接口的使用区别?

继承Thread类,使用起来比较方便,可以直接使用Thread类中的一些方法,但是不能实现资源共享。

实现Runnable接口,会让编写更具有统一性,解决了继承的局限性,可以实现线程之间的资源共享。

Runnable和Callable的区别

相同点:都是接口,都可以编写多线程程序,都需要通过Thread.start()方法启动

不同点:

1.Runnable接口的run方法没有返回值;

Callable接口的call方法有返回值,是一个泛型,可以结合FutureTask对象获取对象的返回值。

2.Runnable接口的run方法只能抛出运行时异常,只能用try/catch

线程执行的生命周期

新生状态:new一个线程对象,该线程对象就处于新生状态,这个时候,该线程对象具有自己的内存空间。

就绪状态:新生状态的线程对象,调用了start方法之后,进入就绪状态,这时候,线程具有运行条件,再等待CPU分配时间片。

如果系统选定了一个就绪状态的线程,就会从就绪状态,进入执行状态,这个动作称为CPU调度。

运行状态:获取到执行的时间片,这时候会执行线程对象中的run方法,直到任务完成后死亡,或者等待资源阻塞。

阻塞状态:运行状态下的线程,执行力sleep方法或者io资源阻塞、wait方法、synchronized同步作用下会进入阻塞状态。

阻塞状态下,线程不能进入就绪队列,只有阻塞原因清除,才会重新进入就绪状态中排队等待,被系统选中后,从原来停止的位置,继续执行。

死亡状态:线程正常执行完了它的工作、线程被强制终止了、线程抛出未被捕获的异常,线程都会进入死亡状态。

package com.day17.thread3;

public class ThreadDemo05 implements Runnable{
    @Override
    public void run() {
        System.out.println("我正在运行");
        try {
            Thread.sleep(2000);//休眠2秒
            System.out.println("线程休眠后继续运行");
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("线程被中断");
        }
    }

    public static void main(String[] args) {
        Thread t = new Thread(new ThreadDemo05());
        System.out.println("线程t进入新生状态1");
        t.start();
        System.out.println("线程进入就绪状态2");

    }
}

线程的调度

void join()

等待这个线程死亡。

void setPriority(int newPriority)

更改此线程的优先级。

static void yield()

对调度程序的一个暗示,即当前线程愿意产生当前使用的处理器。

package com.day17.thread3;

public class ThreadDemo06 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }

    public static void main(String[] args) {
        //void setPriority(int newPriority)
        //更改此线程的优先级,并不会一定将线程优先执行完毕
        Thread t1 = new Thread(new ThreadDemo06(), "A");
        Thread t2 = new Thread(new ThreadDemo06(), "B");

//        t1.setPriority(Thread.MAX_PRIORITY);
//        t2.setPriority(Thread.MIN_PRIORITY);
//
//        t1.start();
//        t2.start();

        //void join()
        //等待这个线程死亡。
        t1.start();
        for (int i = 1; i < 10; i++) {
            if (i==5){
                try {
//                    t1.join();//插队
                    System.out.println("线程礼让");
                    //线程礼让方法,执行之后,不代表后面马上会执行其他内容
                    //只是礼让下次执行机会,继续再公平竞争
                    Thread.yield();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
        //static void yield()
        //对调度程序的一个暗示,即当前线程愿意产生当前使用的处理器。

    }
}

void setDaemon(boolean on)

将此线程标记为 daemon线程或用户线程。

当运行的唯一线程都是守护进程线程时,Java虚拟机将退出。

守护线程:运行在后台的一种特殊线程,独立于控制终端并且周期性执行某种任务

setDaemon()这个方法必须在启动线程前调用

比如JVM中垃圾回收、内存管理等线程都是守护线程

//1.设为守护线程后,主线程结束,守护线程也会结束

//2.普通线程,主线程结束后,普通线程还会继续运行

package com.day17.thread3;

public class ThreadDemo07 extends Thread{
    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("守护线程");
        }
    }

    public static void main(String[] args) {
        System.out.println("程序开始");
        ThreadDemo07 t = new ThreadDemo07();
        //1.设为守护线程后,主线程结束,守护线程也会结束
        //2.普通线程,主线程结束后,普通线程还会继续运行
        t.setDaemon(true);
        t.start();

        //主线程休眠5秒
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程运行结束");
    }
}

线程安全问题

在多线程的运行下,程序运行的结果,和我们预期的结果不一致(排除代码错误的情况),

这种情况就可以看作是线程安全问题,每次运行的结果可能都会不同,问题不易复现,

解决比较困难。

package com.day17.thread4;

public class T1A {
    //出现线程安全,这个类中必须得有共享资源
    int num;

    public int getNum() {
        return num;
    }
    public int updateNum(){
        return num++;
    }
}
package com.day17.thread4;

public class T1 extends Thread{
    T1A t1a = new T1A();

    @Override
    public void run() {
        while (true){
            System.out.println("现在运行的是:" +
                               Thread.currentThread().getName() +
                               "num == " + t1a.updateNum());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        T1 t1 = new T1();
        new Thread(t1).start();
        new Thread(t1).start();
        new Thread(t1).start();
        new Thread(t1).start();
    }
}

通过反编译查看线程安全文件的原因:

其实,出现线程安全问题的原因就是,代码执行的过程并不是原子性的操作,一个线程在执行的时候,会有其它线程读取到中间步骤或者读取到未修改的值,所以导致线程安全问题的出现。

通过synchronized关键字解决线程安全问题。

package com.day17.thread4;

import java.util.ArrayList;

/*
线程同步示例
 */
public class ThreadDemo08 {
    public static void main(String[] args) {
        //线程不安全的List
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 20000; i++) {
            new Thread(
                ()->{
                    synchronized (list){
                        list.add(Thread.currentThread().getName());
                    }
                }
            ).start();
        }
        for (int i = 5; i > 0; i--) {
            try {
                Thread.sleep(1000);
                System.out.println("倒计时:"+i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(list.size());
    }
}

什么样的情况,会发生线程安全问题?

1.多线程环境

2.存在共享资源,多个线程会去访问共享资源

3.存在并发修改共享资源的情况

线程同步

1、为什么要使用多线程完成并发编程?

多线程可以充分利用CPU计算能力,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,提升性能。

方便进行业务拆分,提升系统的并发能力和性能。

2、并发编程的缺点

并发编程虽然能提高效率,但是也会产生各种问题,比如:线程安全、死锁、上下文切换、内存泄露...

3、并发编程三要素【面试】

1.原子性:保证程序执行的步骤不可再分,要么全部成功要么全部失败。

2.可见性:一个线程对共享资源的修改,另一个线程可以马上看到。

(synchronized、volatile)

3. 有序性:程序执行的顺序按照代码的先后顺序执行(JVM中可能会对指令进行重排序) 

线程切换,可能会带来原子性问题。

缓存导致可见性问题。

编译优化带来有序性问题。

以上三个问题的出现,就是线程安全问题的原因。

Java中解决线程安全问题

原子性:synchronized、Lock、JDK、Atomic开头的原子类 可以解决原子性问题

可见性:synchronized、volatile、Lock 可以解决可见性问题

有序性:Happens-Before规则 解决有序性问题

并发相关的概念

并发:多个任务在同一个CPU核上,按照时间片轮流执行,从逻辑上看,多个任务是同时执行的。

并行:单位时间内,多个处理器同时处理多个任务,是真正的同时执行。

串行:有多个任务,由同一个线程按照顺序执行,由于任务、方法都在一个线程下执行,不存在线程不安全的情况。

高并发:同一个对象,被多个对象同时操作,存在线程安全问题,可以通过线程同步来解决并发问题。

线程同步:其实就是一种等待机制,多个需要同时访问某个对象的线程,进入对象的等待池中,形成队列,前面的线程用完了,下个线程再使用。

Java中怎么实现线程同步?

  Java为了保证数据访问的安全性,在多线程中可以加入锁机制synchronized,

  如果一个线程获取到锁,把资源独占,其他线程等待前面线程用完之后释放锁。

synchronized关键字的用法

synchronized可以用来控制线程同步,在多线程情况下,加了这个关键字的代码,不会被多个线程执行。

synchronized可以用来修饰类、方法、变量。

早期版本的synchronized比较笨重,jdk1.6之后引入了大量的优化。

常用的写法:
1、直接写在方法上,称为同步方法

public synchronized void method(){}

每个synchronized方法,必须获取到调用该方法的对象的锁才能执行,否则该线程处于阻塞状态,

如果线程拿到锁了,就能独占该锁,直到方法运行结束,释放锁,其他的被阻塞的线程才能获取锁。

package com.day17.thread4;

/*
初识并发问题,模拟抢票案例
 */
public class TicketDemo implements Runnable{
    //设置票数,票数是公共的资源,多个线程共享
    private int ticketNumber = 10;
    boolean flag=true;

    @Override
    public void run() {
        while (flag){
            buyTicket();
            try {
                //模拟网络延迟
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
    //写一个买票方法
    public synchronized void buyTicket(){
        if (ticketNumber <= 0){
            flag=false;
            return;
        }

        //        try {
        //                //模拟网络延迟
        //                Thread.sleep(100);
        //        } catch (InterruptedException e) {
        //                e.printStackTrace();
        //        }
        System.out.println(Thread.currentThread().getName()+
                           "->抢到了第" + ticketNumber-- +"张票");
    }


    public static void main(String[] args) {
        TicketDemo t = new TicketDemo();
        //把实现类对象传入到Thread对象中,创建Thread对象
        new Thread(t,"小明").start();
        new Thread(t,"小红").start();
        new Thread(t,"黄牛").start();
    }
}
2、修饰静态方法

当synchronized静态方法上的时候,就是相当于给类加锁,锁的是每个类的Class类对象, 会作用于所有对象的实例。

3、使用synchronized修饰代码块,成为同步代码块

synchronized(obj){}

同步代码块一般需要传入一个参数,这个参数(obj)可以是任意对象,将来一般将具有共享资源的对象放在这里,作为同步监视器。

package com.day17.thread4;

public class TicketDemo02 implements Runnable{
    //设置票数,票数是公共的资源,多个线程共享
    private int ticketNumber = 10;
    boolean flag=true;
    Object object = new Object();
    @Override
    public void run() {
        while (flag){
            buyTicket();
            try {
                //模拟网络延迟
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    //写一个买票方法
    public void buyTicket(){
        synchronized (object){
            if (ticketNumber <= 0){
                flag=false;
                return;
            }

            System.out.println(Thread.currentThread().getName()+
                               "->抢到了第" + ticketNumber-- +"张票");
        }

    }


    public static void main(String[] args) {
        TicketDemo02 t = new TicketDemo02();
        //把实现类对象传入到Thread对象中,创建Thread对象
        new Thread(t,"小明").start();
        new Thread(t,"小红").start();
        new Thread(t,"黄牛").start();
    }
}
synchronized优缺点

优点:解决了线程安全问题

缺点:性能下降,影响效率,修饰方法的话,每次调用方法就会产生锁,浪费资源,可能产生死锁

Lock锁

JDK1.5开始,提供的一个更强大的线程同步锁

通过显式的定义同步锁对象来实现同步,也就是Lock对象。

锁是用于通过多个线程控制对共享资源的访问的工具。

通常,锁提供对共享资源的独占访问:

一次只能有一个线程可以获取锁,并且对共享资源的所有访问都要求首先获取锁。

使用方式:

使用Lock接口的常用实现类ReentrantLock

ReentrantLock是一个可重入互斥Lock,具有与使用synchronized方法

和语句访问的隐式监视锁相同的基本行为和语义,但具有扩展功能

语法:

class X {

private final ReentrantLock lock = new ReentrantLock();

// ... public void m() {

lock.lock();//手动调用lock方法加锁

try {

// ... method body

}

finally {

lock.unlock() ; //释放锁

}

}

}

package com.day17.thread4;
/*
Lock
 */
import java.util.concurrent.locks.ReentrantLock;

public class TicketDemo03 implements Runnable{
    //设置票数,票数是公共的资源,多个线程共享
    private int ticketNumber = 20;
    boolean flag=true;
    ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        while (flag){
            buyTicket();

        }

    }
    //写一个买票方法
    public  void buyTicket(){
        try {
            lock.lock();
            if (ticketNumber <= 0){
                flag=false;
                return;
            }
            System.out.println(Thread.currentThread().getName()+
                               "->抢到了第" + ticketNumber-- +"张票");
            //模拟网络延迟
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }


    public static void main(String[] args) {
        TicketDemo03 t = new TicketDemo03();
        //把实现类对象传入到Thread对象中,创建Thread对象
        new Thread(t,"小明").start();
        new Thread(t,"小红").start();
        new Thread(t,"黄牛").start();
    }

}

Lock和synchronized的区别

1.synchronized 是一个关键字,Lock是一个接口,ReentrantLock是一个类,类中提供了比synchronized更多灵活性的操作,比如,可以被继承、可以有方法、可以有各种类变量

2.synchronized早期效率比较低,jdk1.6之后做了优化后,性能较好

3.ReentrantLock使用起来比较灵活,但是必须要有开启锁和释放锁的动作,synchronized不需要手动开启、释放锁

4.ReentrantLock一般只适用于代码块锁,而synchronized可以用于修饰类、变量、方法

Lock和synchronized选择用哪个?

因为JDK1.6之后,synchronized做了很多优化,所以synchronized效率不比Lock,而且使用更加简单,一般情况下,就使用synchronized

死锁

概念

指的是两个或者两个以上的线程中,在执行过程中,由于竞争资源或者由于彼此通信造成的一种阻塞现象,如果没有外力作用,他们无法推进下去,此时这种状态称为系统的死锁,这些永远在互相等待的线程称为死锁线程。

出现死锁后,程序不会有异常,也不会出现提示,只是线程处于阻塞状态无法继续,将来编写程序,要避免死锁的发生。

出现死锁的条件

1.互斥条件:线程对于分配到的资源具有排他性,也就是一个资源只能被一个线程占用,直到被该线程释放。

2.请求与保持条件:一个线程因为请求被占用资源而发生阻塞时,对已经获取的资源,保持不放。

3.不剥夺条件:线程在获取资源后,未使用完之前不能被其他线程强行剥夺。

4.循环等待条件:当发生死锁的时候,所有等待的线程必定会形成一个环路(类似于死循环),造成永久阻塞。

怎么避免死锁?

破坏上面四个条件中的任意一个,就不会产生死锁。

package com.day17.deadLock5;

//茶杯对象
public class TeaCup {
}


package com.day17.deadLock5;

//牙膏对象
public class ToothPaste {
}
package com.day17.deadLock5;

public class BrushTooth implements Runnable{
    String name;//谁拿了对象
    int choice;//0:茶杯;1:牙膏

    static TeaCup teaCup = new TeaCup();
    static ToothPaste toothPaste = new ToothPaste();

    public BrushTooth(String name, int choice) {
        this.name = name;
        this.choice = choice;
    }

    @Override
    public void run() {
        try {
            brushTooth();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //写一个刷牙方法
    public void brushTooth() throws InterruptedException {
        //先拿茶杯,再拿牙膏
        if (choice == 0){
            synchronized (teaCup){
                System.out.println(name+"拿到了茶杯,等待拿牙膏刷牙!");
                Thread.sleep(1000);
                synchronized (toothPaste){
                    System.out.println(name+"拿到牙膏,开始刷牙!");
                }
            }
        }else {
            //先拿牙膏,再拿茶杯
            synchronized (toothPaste){
                System.out.println(name+"拿到了牙膏,等待拿茶杯刷牙!");
                Thread.sleep(1000);
                synchronized (teaCup){
                    System.out.println(name+"拿到茶杯,开始刷牙!");
                }
            }

        }
    }
}
package com.day17.deadLock5;

public class Test {
    public static void main(String[] args) {
        BrushTooth t1 = new BrushTooth("A", 0);
        BrushTooth t2 = new BrushTooth("B", 1);
        new Thread(t1).start();
        new Thread(t2).start();
    }
}

volatile关键字

Java提供了volatile关键字用来解决多线程中的部分问题。

volatile可以用来保证可见性和有序性,不能保证原子性,还会出现线程安全问题。

package com.day17.thread6;

public class ThreadDemo09 {
    public int num=0;
    public  boolean flag = false;
    //不加volatile,程序一直在运行
    //volatile保证了类属性在多个线程中的可见性
    //public volatile boolean flag = false;

    //synchronized也可以保证可见性
    //通过给方法修饰后,间接控制属性可见性


    public synchronized boolean isFlag() {
        return flag;
    }

    public synchronized void setFlag(boolean flag) {
        this.flag = flag;
    }

    public synchronized void addNum(){
        num++;
    }

    public int getNum() {
        return num;
    }

    public static void main(String[] args) {
        ThreadDemo09 t1 = new ThreadDemo09();
        new Thread(
            ()->{
                for (int i = 0; i <5; i++) {
                    t1.addNum();
                    System.out.println("addNum方法被调用" + i);
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //将flag设为true
                //                    t1.flag=true;
                t1.setFlag(true);
                System.out.println("已经将flag设为true了。");
            }
        ).start();

        new Thread(
            ()->{
                System.out.println("线程2正在执行");
                //                  while (!t1.flag){
                //                  }
                while (!t1.isFlag()){

                }
                System.out.println("num的值是"+t1.getNum());
            }
        ).start();
    }
}
package com.day17.thread6;

public class ThreadDemo10 {
    //volatile不能保证原子性
    public volatile int num=0;
//    public int num=0;
    //    public synchronized void addNum(){
    public void addNum(){
        num++;
    }

    public int getNum() {
        return num;
    }

    public static void main(String[] args) {
        ThreadDemo10 t1 = new ThreadDemo10();
        //循环10次,每次创建一个子线程,每个子线程做了100次 num++,
        //如果有原子性的,线程安全的,最终的值应该是1000
        //在属性前加上volatile不能保证原子性,所以最终的值不是1000
        //使用synchronized关键字,加在方法前面,可以保证原子性
        for (int i = 0; i < 10; i++) {
            new Thread(
                    ()->{
                        for (int j = 0; j < 100; j++) {
                            t1.addNum();
                            try {
                                Thread.sleep(1);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
            ).start();
        }
        //获取值
        for (int i = 0; i < 10; i++) {
            System.out.println("num的值是:"+t1.getNum());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

synchronized和volatile的区别

synchronized表示只有一个线程可以获取作用对象的值,执行代码会阻塞其他线程。

volatile表示变量在CPU的寄存器中是不确定的,必须从内存中读取,保证多线程下变量的可见性,

禁止指令重排(有序性)。

区别:

1.volatile是用来修饰变量的,synchronized可以修饰类、方法、变量。

2.volatile只能实现变量的可见性,不能保证原子性;synchronized可以保证原子性和可见性。

3. volatile不会造成线程阻塞,synchronized会造成线程阻塞。

4.volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化。

5.volatile性能比synchronized要好。

线程交互

Object类中的方法 wait()、notify()、notifyAll()

wait()方法:释放占有的对象锁,线程进入等待池,释放CPU,

其他正在等待的线程可以拿到锁,获取到锁的线程可以执行程序。

notify()方法:该方法会唤醒因为调用wait()方法而等待的线程,其实就是对 对象锁 的唤醒,

被唤醒的线程可以有机会继续获得对象锁,从而执行程序。

另一个线程调用notify()后,不会马上释放锁,继续把代码执行完毕,才会释放锁。

notifyAll()方法:所有线程

void notify()

唤醒正在等待对象监视器的单个线程。

void notifyAll()

唤醒正在等待对象监视器的所有线程。

void wait()

导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法。

注意:wait()和notify()都需要synchronized中调用。

因为wait()就是用来释放锁的,线程必须拥有该对象的锁,才能去释放锁。

package com.day17.thread9;

public class WaitDemo01 implements Runnable{
    int count=0;

    @Override
    public void run() {

        while (count<10){
            synchronized (ThreadWait.obj){
                //线程第一次进来,没有线程等待,不需要执行唤醒
                if (count!=0){java
                              ThreadWait.obj.notify();
                             }
                System.out.println("A"+count);
                try {
                    ThreadWait.obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                count++;
            }
        }
    }
}
package com.day17.thread9;

public class WaitDemo02 implements Runnable{
    int count=0;

    @Override
    public void run() {
        while (count<10){
            synchronized (ThreadWait.obj){
                //唤醒线程1,唤醒之后会继续执行当前代码
                //代码执行完,才会释放锁
                ThreadWait.obj.notify();
                System.out.println("B"+count);
                //当程序运行到9,表示循环结束了,不需要再等待了
                if (count != 9){
                    try {
                        ThreadWait.obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                count++;
            }
        }
    }
}
package com.day17.thread9;

public class ThreadWait {
    //创建一个对象
    public static final Object obj = new Object();

    public static void main(String[] args) {
        new Thread(new WaitDemo01()).start();
        new Thread(new WaitDemo02()).start();

    }
}

【面试题】wait()和sleep()的区别?

相同点:都可以让线程暂停,进入阻塞状态。

不同点:

1.所属类不同,wait()方法属于Object类的一个方法,sleep()是属于Thread类的一个方法。

2.锁释放不同:wait()会释放锁,sleep()不会。

3.用途不同:wait()常被用于线程间的交互(通信),sleep()一般用来暂停线程的执行。

4.用法不同:wait()方法被调用,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()方法或者notifyAll()方法进行唤醒操作,如果调用wait(long timeout) ,超时后会自动唤醒。

sleep()执行完之后,线程会自动苏醒。

生产消费者模型(基于wait方法的一个应用模型)

不是面向对象的设计模式

也可以称为(生产者 - 消费者 - 仓库)模型

1.生产者,在仓库未存满的时候,开始生产商品,仓库满了,停止生产

2.消费者,在仓库有产品的时候,才能消费,仓库空了,则等待

3.当消费者发现仓库没有产品,会通知生产者生产

4.当生产者生产可以消费的产品,则通知消费者消费。

这个模型有哪些类?

生产者,就有生产方法

消费者:具有消费方法

仓库:添加产品,减少产品

产品

package com.day17.thread10;
//炸鸡产品
public class Chicken {
    int number;

    public Chicken(int number) {
        this.number = number;
    }

}
package com.day17.thread10;
//仓库
public class Warehouse {
    //表示仓库中最多可以被存放多少个产品
    Chicken[] chickens = new Chicken[10];
    //计数变量,用来记录产品数量
    int num=0;

    //取出产品
    public synchronized Chicken pop() {
        //如果仓库中没有产品,消费者等待
        while (num<=0){
            try {
                this.wait();//消费者等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        num--;
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Chicken chicken = chickens[num];
        this.notifyAll();
        return chicken;
    }
    //添加产品
    public synchronized void put(Chicken chicken) {
        //如果产品满了,不生产,等待消费
        while (num >= chickens.length-1){
            try {
                this.wait();//停止生产,等待消费
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //产品没满,就通知生产者生产
        chickens[num] = chicken;
        num++;
        this.notifyAll();

    }

}
package com.day17.thread10;
//生产者
public class Producer implements Runnable{

    //声明一个仓库对象,通过仓库对象调用方法
    Warehouse warehouse = new Warehouse();

    public Producer(Warehouse warehouse) {
        this.warehouse = warehouse;
    }

    //生产方法
    @Override
    public void run() {
        for (int i = 1; i <= 50; i++) {
            //生产产品其实是在调用仓库对象,往仓库中添加产品
            warehouse.put(new Chicken(i));
            System.out.println("生产者生产了第"+ i +"号炸鸡!");
        }
    }


}
package com.day17.thread10;
//消费者
public class Consumer implements Runnable{

    //声明一个仓库对象,通过仓库对象调用方法
    Warehouse warehouse = new Warehouse();

    public Consumer(Warehouse warehouse) {
        this.warehouse = warehouse;
    }

    //取出方法
    @Override
    public void run() {
        for (int i = 1; i <= 50; i++) {
            //消费产品其实是在调用仓库对象,从仓库中取出产品
            Chicken chicken = warehouse.pop();
            System.out.println("消费者消费了了第"+ chicken.number +"号炸鸡!");
        }
    }
}

package com.day17.thread10;

public class Test {
    public static void main(String[] args) {
        Warehouse warehouse = new Warehouse();

        new Thread(new Producer(warehouse)).start();
        new Thread(new Consumer(warehouse)).start();

    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1678500.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

javafx设置启动按钮运行项目

1.点击这里 2.执行图中4步操作&#xff0c;点击ok

OpenAI 重磅发布GPT 4o!可以视频聊天的AI?

OpenAI 重磅发布GPT 4o&#xff01; 前言 就在今日&#xff0c;OpenAI发布了ChatGPT-4o版本&#xff0c;技术主管 Mira Murati 在直播中表示GPT-4o对比之前版本速度更快&#xff0c;在文本、视频和音频方面的能力也都有所提高。值得注意的是它还可以让用户与 ChatGPT 进行视频聊…

Jmeter接口测试和Jmeter接口自动化测试

一、Jmeter 的使用步骤 打开Jmeter 安装包&#xff0c;进入\bin 中&#xff0c;找到"jmeter.bat", 点击打开即可。 在下图打开的Jmeter 页面中&#xff0c;右键“测试计划” -> “添加” -> "Threads(Users)" -> “线程组”&#xff0c; 建立线程…

DBeaver如何csv导入数据

简言之先要创建任务&#xff0c;任务还需要去执行&#xff0c;只有执行之后才是执行真的导入了 那个保存任务真的很误导人啊 1.首先点击你要被导入的表&#xff0c;右键选择导入数据然后选择直接点击下一步,这个地方需要修改格式&#xff0c;否则会乱码 如果你导入的没有标题…

IO系列(四) - RandomAccessFile 类解读

一、摘要 RandomAccessFile 类&#xff0c;也被称为随机访问文件类。 RandomAccessFile 可以说是 Java 体系中功能最为丰富的文件操作类&#xff0c;相比之前介绍的通过字节流或者字符流接口方式读写文件&#xff0c;RandomAccessFile 类可以跳转到文件的任意位置处进行读写数…

买了个彩票,哈哈哈哈哈。

买了个彩票-双色球&#xff0c;发现挺有意思的。 索性把双色球的所有期的中奖号码的数据都爬了下来&#xff0c;03至今&#xff0c;21年了。txt文本&#xff0c;6.5MB大小。 大家有啥好的建议&#xff0c;分析一下数据呢。

字节跳动在2024年春季火山引擎Force原动力大会上隆重推出了“豆包大模型”家族

此次大会以AI为主题&#xff0c;聚焦大模型的应用与发展&#xff0c;旨在引领AI技术的落地和推动各行各业的数字化转型。 字节跳动官网&#xff1a;https://www.bytedance.com/zh/ 豆包官网&#xff1a;https://www.doubao.com/chat/ 更多消息&#xff1a;https://heehel.co…

洗衣洗鞋店做小程序有什么优势?

互联网洗衣洗鞋小程序闪亮登场&#xff0c;想知道这款小程序有何魅力吗&#xff1f; 如今&#xff0c;众多商家纷纷推出预约上门洗鞋服务&#xff0c;&#x1f481;‍♀️并倾力打造洗鞋小程序&#xff0c;旨在拓展线上销售渠道。&#x1f31f;那么&#xff0c;这款洗鞋小程序究…

2025秋招Java还是c++?

一、我的编程经 说说我的编程经历&#xff0c;在C和Java之间我经历了几个阶段&#xff1a; 大学期间&#xff0c;我浅尝辄止地学习了一段时间的Java&#xff0c;但后来放弃了&#xff0c;开始学习C/C。本科毕业后&#xff0c;我选择攻读硕士学位&#xff0c;并一直专注于C的学…

3种深拷贝实现,你都知道吗?

目录&#xff1a; 1、JSON.parse 2、structuredClone 3、cloneDeep

冒险岛vcruntime140_1.dll无法继续执行代码要怎么处理?教你一键修复vcruntime140_1.dll

当你在玩着冒险岛的时候&#xff0c;突然弹出一个vcruntime140_1.dll无法继续执行代码&#xff0c;这时候你是不是一脸懵逼&#xff1f;不知道怎么去解决&#xff1f;其实不需要担心&#xff0c;这是一个小问题&#xff0c;vcruntime140_1.dll文件是一个非常常用的dll文件&…

企业架构系统之-IT系统建设如何做好技术选型

背景 近日有幸与行业同仁交流工作心得&#xff0c;在讨论中&#xff0c;他们提到一个平时工作当中我们都会遇到和经历的一个问题&#xff1a;作为架构师&#xff0c;在日常工作中应如何进行技术选型&#xff1f;面对众多框架和组件中&#xff0c;我们又应如何选择&#xff0c;…

【更新公告】AirtestIDE更新至1.2.17版本

本次更新为AirtestIDE、Airtest-Selenium库更新。 AirtestIDE更新至1.2.17版本&#xff0c;AirtestIDE内置库Airtest更新为1.3.3.1版本&#xff0c;Poco更新为1.0.94版本&#xff0c;主要支持了selenium4.0以上版本&#xff0c;ADB更换为41版本&#xff0c;Airtest新增点击和滑…

“网络安全新纪元:等保2.0的详细解读与实践”

网络安全等级保护基本要求》&#xff08;等保2.0&#xff09;于2019年6月发布&#xff0c;是我国网络安全等级保护制度的一项重要标准。等保2.0主要针对关键信息基础设施的网络安全保护&#xff0c;对数据安全和个人信息保护提出了更高的要求。本文将对等保2.0进行详细解读&…

Pytorch入门实战 P10-使用pytorch实现车牌识别

目录 前言 一、MyDataset文件 二、完整代码&#xff1a; 三、结果展示&#xff1a; 四、添加accuracy值 &#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制 本周的学习内容是&#xff0…

【永洪BI】管理系统

管理系统模块包括系统设置、认证授权、日志管理、监控预警、资源部署、VooltDB管理、数据库管理、企业应用配置、系统检查、应用管理模块。 系统设置界面&#xff1a; 可以进行清除系统缓存、配置系统主题、配置系统邮箱、配置门户主页、配置权限管理系统、配置密码策略、配置…

端午佳节,品尝食家巷传统面点与黄米粽子礼盒

端午佳节&#xff0c;品尝食家巷传统面点与黄米粽子礼盒 在这个端午节来临之际&#xff0c;食家巷倾情推出一款别具特色的端午礼盒&#xff0c;将甘肃的传统面点与地方特色黄米粽子完美融合&#xff0c;为您带来一场美味与传统的邂逅。 这款礼盒以甘肃传统面点一窝丝、油饼和烤…

Python 渗透测试:子域名查询.

什么是 子域名查询. 子域名查询是指通过域名系统(DNS)查找某个域名下的子域名信息。子域名是域名层级结构中的一部分,位于主域名的下一级。子域名查询是网络安全评估和渗透测试中的一个重要步骤,可以帮助安全研究人员更好地了解目标系统的架构和潜在的安全隐患。但在进行子域名…

svn批量解锁

问题 svn对文件进行checkout之后&#xff0c;先进行lock&#xff0c;之后再去更改&#xff0c;最后进行Commit操作&#xff1b; 上述为我们通过svn管理代码的正常方式&#xff0c;但总会有其他现象发生&#xff1b; 如果我们非正常操作&#xff0c;批量锁所有的svn文件&#x…

Pencils Protocol 宣布再获合作伙伴 Galxe 的投资

近日&#xff0c;Scroll生态项目Penpad将品牌进一步升级为Pencils Protocol&#xff0c;全新升级后其不仅对LaunchPad平台进行了功能上的升级&#xff0c;同时其也进一步引入了Staking、Vault以及Shop等玩法&#xff0c;这也让Pencils Protocol的叙事方向不再仅限于LaunchPad&a…