17.多线程

多线程

程序、进程、线程的概念

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

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

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

线程:一个进程中,可以包含若干个线程,至少包含一个线程。线程是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();

    }
}
  • 29
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值