Java多线程超详细笔记

简介: Java多线程超详细笔记

Java多线程超详细笔记


引言:

本文主要介绍了线程的基本概念、三种实现方式、线程的7种状态、常用的方法(setName()、setDaemon()、join()、sleep()、yide()、 interrupt()等等)、线程的安全问题(synchronized锁)、线程的通讯以及线程中的经典问题(死锁、生产者和消费者)。

  • Java高级多线程:属于本文的补充内容,分享了多线程中的线程池概念以及获取线程池的方法、Callable接口、Future接口、线程的同步以及异步、ReentrantLock(重入锁)、ReentrantRaedWriteLock(读写锁)
  • Java中线程安全的集合:分享了Collection体系集合下除了Vector以外的线程安全集合,包括:Collection中的安全工具方法、CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap、Queue接口

@[toc]

1. 进程与线程

            进程是程序的一次动态执行过程,只有真正运行时的程序,才被称为进程,能够调用系统资源的独立单位;多进程的操作系统能同时运行多个进程,由于CPU具有分时功能,所以每个进程都能循环获得自己的时间片(两个进程在互相抢占CPU的执行权)。由于CPU执行的速度较快,所以感觉所有程序是同时进行,是系统进行资源分配和调用的独立单位,每一个进程都有它自己的内存空间和系统资源。

            线程是依赖于进程存在的,每一条线程就是一条执行路径;跟进程一样都是实现并发的基本单位。线程是比进程更小的执行单位,所谓多线程就是指一个进程在执行过程中可以产生多个更小的程序单元,这些更小的程序单元称为称为线程,线程可以同时存在、同时运行,一个进程可能包含了多个同时执行的线程

            Java中的多线程是指程序的运行流程,多线程机制是指可以同时运行多个程序块,使程序的速率更高。

并发、并行(同时的意思)

  • 并发:指的物理上的同时,在一个时间点上同时执行多个执行程序 (高并发)
  • 并行:指的逻辑上的同时,在一个时间段内同时执行多个执行程序

2. 线程的组成

任何一个线程都有基本组成部分:

  • CPU时间片:OS会为每个线程分配执行时间。
  • 运行数据:
    • 堆空间:存储线程需使用的对象,多线程可以共享堆中的对象。
    • 栈空间:存储线程需要使用的局部变量,每个线程都拥有独立的栈。
  • 线程的逻辑代码。

3. Java中线程的实现

3.1 继承Thread类_01

一个类只要继承了Thread就是多线程的实现类,必须重写run方法;

步骤:

  • 自定义一个类,这个类继承自Thread类

  • 在这个类重写Thread类中的run方法(一般存储耗时的代码)

  • 在主线程中开启子线程 main线程

    • 创建自定义线程类对象
    • 使用线程类对象调用start()方法
public class TestExtendsThreads {
   
   
    public static void main(String[] args) {
   
   
        //创建线程对象
        MyThread1 t1 = new MyThread1();
        MyThread2 t2 = new MyThread2();
        //t1.run();//普通对象调用方法
        t1.start();//线程启动(JVM调用run方法)
        t2.start();
        for(int i = 1; i<50; i++){
   
   
            System.out.println("Main:"+i);
        }
    }
}

//线程类 自定义线程
class MyThread1 extends Thread{
   
   
    public void run(){
   
   
        //线程的执行具有随机性,存储"耗时代码"
        for(int i = 1; i<50; i++){
   
   
            System.out.println("MyThread1:"+i);
        }
    }
}
class MyThread2 extends Thread{
   
   
    public void run(){
   
   
        for(int i = 1; i<50; i++){
   
   
            System.out.println("MyThread2:"+i);
        }
    }
}
  • 一个类通过继承Thread类来实现,start()方法只能调用一次,如果调用多次就是抛出IllegalThreadStateException异常;

3.2 实现Runnable接口_02

只需要实现一个抽象方法:public void run();

步骤:

  • 自定义一个类 , 实现 Runnable接口
  • 实现接口中提供的功能:public abstract void run() ; 比较耗时的操作
  • 用户线程main,创建资源类对象
    • 创建Thread类对象,将上面对象作为参数传递
    • 分别启动线程

Thread类的构造方法

  • public Thread(Runnable target):分配一个新的Thread对象
  • public Thread(Runnable target, String name):分配一个新的Thread对象

  • Thread.currentThread()获取正在运行的线程

public class TestImplementsRunnable {
   
   
    public static void main(String[] args) {
   
   
        //创建资源类对象象
        MyRunnable mr1 = new MyRunnable();
        //创建线程类对象
        Thread t1 = new Thread(mr1);
        Thread t2 = new Thread(mr1);
        //分别启动线程
        t1.start();
        t2.start();
        for(int i = 1; i<50; i++){
   
   
            System.out.println("Main:"+i);
        }
    }
}

class MyRunnable implements Runnable{
   
   
    public void run(){
   
   
        for(int i = 1; i<50; i++){
   
   
                //Thread.currentThread()正在运行的线程
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
  • Runnable接口必须要定义一个run的无参数方法
  • 实现接口,只是将当前类编成任务类,本身不是个线程
  • 更灵活、提供了能力、不影响继承

3.3 线程池方式(实现Callable接口)_03

步骤:

  • 创建线程池对象: ExecutorService pool = Executors.newFiexdThreadPool(int nThreads){}
  • 提交异步任务 submit(Callable call)
    • Callable:接口 (Functional Interface:函数式接口:接口中只有一个抽象方法)
    • V call():计算结果
  • 关闭资源:shutdown():关闭线程池资源对象
public class MyCallable implements Callable {
   
   
    @Override
    public Object call() throws Exception{
   
   
        for(int i = 0 ;i < 200; i++){
   
   
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
        return null;
    }
}

public static void main(String[] args) {
   
   
    //创建线程池对象
    ExecutorService pool = Executors.newFixedThreadPool(2);
    //提交异步任务
    pool.submit(new MyCallable());
    pool.submit(new MyCallable());

    //关闭资源 
    pool.shutdown();
}

4. Thread类和Runnable接口

Thread也是Runnable类的子类,如果一个类继承Thread类,则不适合多个线程共享资源,但是实现了Runnable接口可以很好实现资源共享;

案例:设计一个程序,电影院卖票,有3个窗口共同出售100张门票

4.1 继承自Thread类

//线程类
public class SellTicket extends Thread {
   
    

    public void run() {
   
    
        //定义票
        int tickets = 100 ;
        while(true) {
   
   
            //判断票大于0,一直售票
            if(tickets >0) {
   
   
                System.out.println(this.getName()+",正在出售第"+(tickets--)+"张门票");
            }
        }
    }
}

public static void main(String[] args) {
   
   

    //创建线程类对象
    SellTicket st1 = new SellTicket() ;
    SellTicket st2 = new SellTicket() ;
    SellTicket st3 = new SellTicket() ;

    //设置线程的名称
    st1.setName("窗口1");
    st2.setName("窗口2");
    st3.setName("窗口3");

    //分别启动线程
    st1.start();
    st2.start();
    st3.start();
}
  • 由运行结果可以看出:3个窗口分别出售了100张票,没有达到资源共享;

4.2 实现Runnable接口

public class SellTicket implements Runnable {
   
   
    //定义100张票
    public static int tickets = 100 ;

    public void run(){
   
   
        //模拟一直有票
        while(true) {
   
     
                if(tickets>0){
   
    
                System.out.println(Thread.currentThread().getName()+"正在出售第"+(tickets--)+"张票");                        
            }
        }
    } 
}


public static void main(String[] args) {
   
   
    //创建资源类对象
    SellTicket st = new SellTicket() ;

    //创建三个子线程
    Thread t1 = new Thread(st, "窗口1") ;
    Thread t2 = new Thread(st, "窗口2") ;
    Thread t3 = new Thread(st, "窗口3") ;

    //分别启动线程
    t1.start();
    t2.start();
    t3.start();
}
  • 根据结果可以发现3个窗口总共卖出了100张票,实现了共享;

实现Runnable接口比继承Thread类的优势:

  • 适合多个相同的程序代码的线程去处理同一个资源
  • 可以避免Java单继承带来的局限性
  • 代码可以被多个线程共享,提高了健壮性

5. 线程的状态

实现多线程,就需要在主线程创建线程对象,但是线程因为需求的不同往往不是理性状态;

5.1 线程的状态_基础

线程的状态_基础

  1. New(初始状态):程序在构造方法中创建线程对象后,新的线程对象便是新建状态;
  2. Ready(就绪状态):初始状态调用start()方法就可以启动线程进入就绪状态等待OS选中,并分配时间片;
  3. Running(运行状态):当被OS选中并且获得了时间片之后就会进入该状态,如果时间片到期就会回到就绪状态,要想继续执行只能等待下一次被OS选中;
  4. Terminated(终止状态):当所有的代码都执行完毕后由主线程或独立线程调用run()方法结束后,进入该状态,并释放时间片,处于终止状态的线程比具有继续运行的能力;

5.2 线程的状态_等待

但是在真正执行过程中往往不会是理想的状态,我们出门坐公交都会有等车的时候,更何况是线程!因此线程也会有等待的状态:

线程的状态_等待

  1. New(初始状态)
  2. Ready(就绪状态)
  3. Running(运行状态):上面说了当被OS选中并且获得了时间片之后就会进入该状态;
  4. Timed Waiting(限期等待):当调用了sleep(i)方法后就会进入Timed Waiting(限期等待),当所给的i(2000毫秒)到期后就会冲回到Ready,参与时间片的竞争;
  5. Waiting(无限等待):当调用了join()方法后就会进入Waiting(无限等待),类似于插队谁掉用join方法就会优先执行直到该线程执行完毕,其他线程才可以竞争时间片;
  6. Terminated(终止状态)

5.3 线程状态_阻塞

我们在坐车的时候如果运气不好你不光要等,在路上有可能会有堵车的情况,因此我们的线程也会有阻塞的状态!

线程状态_阻塞

  1. New(初始状态)
  2. Ready(就绪状态)
  3. Blocked(阻塞状态):在运行时期加入同步锁synchronized多个线程抢到时间片后在抢锁,谁先抢到锁谁执行,没有拿到锁的只能等待抢到锁的线程释放锁,然后在去竞争锁,竞争到锁从阻塞状态回到就绪状态那时间片执行自己的代码;
  4. Running(运行状态)
  5. Timed Waiting(限期等待)
  6. Waiting(无限等待)
  7. Terminated(终止状态)

注:

  • JDK1.5之后就绪、运行统称为Runnable
  • 线程的运行状态7种行,6种也行;

5.4 线程的状态图例

在这里插入图片描述

6. 线程中常用的方法

在了解完状态后,下面我们就要讨论造成各个状态对应的方法了!

6.1 获取和设置线程名称

线程的名字一般在启动前设置,两个名字可以重复但是一般不这样做;

给子线程设置现成的名称

  • public final void setName(String name):给线程可以设置线程名称
  • public final String getName():获取该线程名称

静态功能:

  • public static Thread currentThread():正在运行的线程
public class MyThread extends Thread{
   
   
    public void run() {
   
   //t1,t2子线程
        for(int i = 0 ; i < 100 ; i ++) {
   
   
            //获取该线程名称
            System.out.println(getName()+":"+i);
        }
    }
}

public static void main(String[] args) {
   
   
    //创建线程类对
    MyThread t1 = new MyThread() ;
    t1.setName("卡卡_01");
    MyThread t2 = new MyThread() ;
    t2.setName("卡卡_02");

    //开启线程
    t1.start();
    t2.start();

    //正在运行的线程    mian主线程
    System.out.println(Thread.currentThread().getName());
}

6.2 设置守护线程

守护线程是运行在后台的一种特殊进程,在程序中只要有一个线程在运行,整个程序不会消失,设置一个守护线程即使程序结束,后台仍然会继续执行;在 Java 中垃圾回收线程就是特殊的守护线程。

  • public final void setDaemon(boolean on):设一个线程是否是一个守护线程 ,参数如果为true守护线程/后台线程
//自定义一个类 继承Thread
public class ThreadDaemon extends Thread {
   
   
    //所有的子线程都会执行run方法
     public void run(){
   
     // 覆写run()方法
            while(true){
   
   
                System.out.println(Thread.currentThread().getName() + "在运行。") ;
            }
    }
}

public static void main(String[] args) {
   
   
    //创建子线程类对象
    ThreadDaemon td = new  ThreadDaemon() ;

    //设置线程名称
    td.setName("守护线程");

    //设置守护线程
    td.setDaemon(true); 

    //启动线程
    td.start();
}
  • 尽管run方法是死循环,但是程序仍然执行完毕,就是因为将其设置为守护线程,在后台运行完毕;
  • 这个方法必须启动线程之前调用,如果这个正在运行的线程是守护线程,jvm会退出

6.3 join()方法

在线程的操作中加入join()方法让一个线程强制执行,此线程运行期间其他线程必须等待此线程执行完毕后才可以继续执行(进入就绪再次竞争时间片);(类似于插队

Public final void join():让其他的线程加入到当前线程

public class TestJoin {
   
   
    public static void main(String[] args) throws InterruptedException {
   
   
        Thread t1 = new Thread(new Task());
        Thread t2 = new Thread(new Task());

        t1.start();
        t2.start();

        for(int i = 1; i<50; i++){
   
   
            System.out.println(Thread.currentThread().getName()+":"+i);//打印主线程
            if(i == 20){
   
   
                System.out.println("Main执行到20,执行t1");
                t1.join();//加入main中,等待t1结束后,mian才再次开始竞争时间片
            }
        }
    }
}
class Task implements  Runnable{
   
   
    public void run() {
   
   
        for(int i = 1; i<50; i++){
   
   
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
  • 当主线程mian执行到20时,t1加入到main中;当前程序只有t1和t2开始竞争直到t1结束main才开始执行;

6.4 sleep()方法

程序中调用sleep方法,可以使得线程暂时休眠;(类似于睡觉,醒来继续竞争

  • public static void sleep(long millis):让该线程休眠millis毫秒
public class TestSleep {
   
   
    public static void main(String[] args) throws InterruptedException {
   
   
        MyThread t1 = new MyThread();
        t1.start();

        MyRunnable run = new MyRunnable();
        Thread t2 = new Thread(run);
        t2.start();
    }
}
//线程类
class MyThread extends Thread{
   
   
    public void run(){
   
   
        for(int i = 1; i<50; i++){
   
   
            //获得名字
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
class MyRunnable implements Runnable{
   
   
    public void run(){
   
   
        for(int i = 1; i<50; i++){
   
   
            if(i % 2 == 0){
   
   
                System.out.println("得到偶数休眠");
                try {
   
   
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
   
   
                    e.printStackTrace();
                }
            }
            //获得名字
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
  • 该程序t1正常执行,执行t2时得到偶数睡1000毫秒在执行;

6.5 yied()方法

在线程操作中,调用yied()方法可以使当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片;(类似与礼让)

  • public static void yield():暂停当前正在执行的线程对象,并执行其他线程。
public class TestYield {
   
   
    public static void main(String[] args) {
   
   
        Thread t1 = new Thread(new Task());
        t1.start();

        for(int i = 1; i<50; i++){
   
   
            System.out.println(Thread.currentThread().getName()+":"+i);
            if(i % 10 == 0){
   
   
                System.out.println("Main主动放弃");
                Thread.yield();//主动放弃当前时间片,回到就绪状态
            }
        }
    }
}
class Task implements Runnable{
   
   
    public void run(){
   
   
        for(int i = 1; i<50; i++){
   
   
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
  • 当main是10的倍数时主动放弃下一次的竞争,让其他线程先执行

6.6. 线程的优先级

Java程序中所有线程在运行之前都会在就绪状态,因此就存在优先级的问题!默认的优先级是5

  • public final void setPriority(int newPriority):更改线程的优先级
  • public final int getPriority():获取优先级
  • 优先级都是自定义的常量
    • public static final int MAX_PRIORITY 10 :最大优先级
    • public static final int NORM_PRIORITY 5 :默认优先级
    • public static final int MIN_PRIORITY 1 :最小优先级
public class ThreadPriority extends Thread {
   
   
    public void run() {
   
   
        for(int x =0 ; x < 10; x ++) {
   
   
            System.out.println(getName()+":"+x);
        }
    }
}

public static void main(String[] args) {
   
   
    //创建三个子线程
    ThreadPriority tp1 = new ThreadPriority() ;
    ThreadPriority tp2 = new ThreadPriority() ;
    ThreadPriority tp3 = new ThreadPriority() ;

    tp1.setName("卡卡_01");
    tp2.setName("卡卡_02");
    tp3.setName("卡卡_03");

    //分别设置优先级
    tp1.setPriority(10); 
    tp3.setPriority(1);

    //启动线程
    tp1.start();
    tp2.start();
    tp3.start();
}
  • 优先级越大的线程抢占CPU的执行权几率大,当然线程的执行具有随机性
  • 优先级的参数最大是10,不能超过10否则会产生IllegalArgumentException: 非法参数异常

6.7 线程停止

当一个线程运行时,另外一个线程可以直接调用interrupt()方法中断其运行状态,也可以是stop()方法;

  • public void interrupt():中断线程一种状态(睡眠,其他状态..)
  • public final void stop():强迫线程停止执行(虽然过时方法,可以用)

interrupt和stop区别:

  • interrupt:中断线程的状态(睡眠状态),依然可以执行线程
  • stop:线程被强迫终止 ,在启动线程之前,如果调用线程的stop方法,不会执行线程
//线程类
public class ThreadStop extends Thread {
   
   
    @Override
    public void run() {
   
   
        System.out.println("开始执行了"+new Date());

        //线程睡眠10秒
        try {
   
   
            Thread.sleep(10000);
        } catch (InterruptedException e) {
   
   
            //e.printStackTrace();
            System.out.println("线程睡眠被中断了...");
        }

        System.out.println("结束执行了"+new Date());
    }
}    

public static void main(String[] args) {
   
   
    //创建线程类对象
    ThreadStop ts = new ThreadStop();
    //睡眠三秒中
    try {
   
   
        Thread.sleep(3000);
        //强迫停止执行线程
        ts.stop(); 
    } catch (InterruptedException e) {
   
   
        e.printStackTrace();
    }
    //执行线程
    ts.start();
}
  • 直接停止
//线程类
public class ThreadInterrupt extends Thread {
   
   
    public void run() {
   
   
        System.out.println("开始执行了"+new Date());

        //线程睡眠10秒
        try {
   
   
            Thread.sleep(10000);
        } catch (InterruptedException e) {
   
   
            System.out.println("线程睡眠被中断了...");
        }
        System.out.println("结束执行了"+new Date());
    }
}    

public static void main(String[] args) {
   
   
    //创建线程类对象
    ThreadInterrupt ts = new ThreadInterrupt();
     //执行线程
    ts.start();
    //睡眠三秒中
    try {
   
   
        Thread.sleep(3000);
    } catch (InterruptedException e) {
   
   
        e.printStackTrace();
    }
    //终段线程
    ts.interrupt();
}
  • 开始执行了Sat Jun 27 11:09:32 CST 2020
    线程睡眠被中断了...
    结束执行了Sat Jun 27 11:09:35 CST 2020

7. 网络图片下载案例

public class ThreadTest extends Thread {
   
   
    //成员变量
    String url;
    String name; 

    public ThreadTest(String url,String name) {
   
   
        this.url = url ;
        this.name = name ;
    }

    //重写run方法
    public void run() {
   
   //tt1,tt2,tt3
        //创建自定义下载器对象
        WebImageDownLoader wid = new WebImageDownLoader() ;
        wid.downLoader(url, name);
        System.out.println("下载后的保存的文件名称为:"+name);
    }

    //用户线程
    public static void main(String[] args) {
   
   

        //创建子线程类对象
        ThreadTest tt1 = new ThreadTest("https://img14.360buyimg.com/pop/s590x470_jfs/t1/129936/16/2622/89764/5eec9a13Ee82c2ab0/15ca1a3a3ed71f78.1jpg.webp", "1.jpg") ;
        ThreadTest tt2 = new ThreadTest("https://img14.360buyimg.com/pop/s590x470_jfs/t1/129936/16/2622/89764/5eec9a13Ee82c2ab0/15ca1a3a3ed71f78.2jpg.webp", "2.jpg") ;
        ThreadTest tt3 = new ThreadTest("https://img14.360buyimg.com/pop/s590x470_jfs/t1/129936/16/2622/89764/5eec9a13Ee82c2ab0/15ca1a3a3ed71f78.3jpg.webp", "3.jpg") ;

        //开启子线程
        tt1.start();
        tt2.start();
        tt3.start();
    }
}

//自定义的下载器
class WebImageDownLoader{
   
   

    //下载方法
    public void downLoader(String url,String name) {
   
   
        try {
   
   
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
   
   
            e.printStackTrace();
            System.out.println("IO异常,网络图片资源下载失败");
        }
    }
}

8. 线程的安全

8.1 线程的不安全性

           线程在运行期间是交替执行的,根据拿到CPU时间片的不同会产生不同的结果;

现有两个线程AB同时对内存的字符串数组做操作,

  • A线程将Hello存入数组的第一个空位
  • B线程将World存入数组的第一个空位

注:是第一个空位不是第一个索引

理想状态:

  • A拿到时间片在0索引处存放Hello,之后B拿到时间片在1索引处存放World;
  • B拿到时间片在0索引处存放Hello,之后A拿到时间片在1索引处存放World;

实际状态:

  • 可能是理想状态的那两种情况
  • 也可能是同时插入0索引,所以不安去

线程不安全:

  • 当多线程并发访问临界资源的时候,如果破坏原子操作,可能会造成数据不一致
  • 临界资源:共享资源也就是同一对象,一次仅允许一个线程使用,才可以保证正确性
  • 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可以打乱

8.2 解决线程安全的基本思想

如何校验一个多线程是否是一个安全?

  • 是否是多线程环境
  • 是否有共享数据
  • 是否有多条语句操作共享数据

主旨就是让程序处在没有安全问题的环境中

  • 把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行。

8.3 同步代码块

所谓代码块就是使用{}括起来的一段代码;如果在代码块中加入synchronized关键字就称为同步代码块;

格式:

synchronized(锁对象){

​ 多条语句对共享数据进行操作(需要同步的代码)

}

  • 每个对象都有一个互斥锁标记,用来分配线程。
  • 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。
  • 当线程退出同步代码块时,会释放相应的互斥标记。
public class SellTicket implements Runnable {
   
   //资源类
    //定义100张票
    public static int tickets = 100 ;
    private Object obj = new Object() ;

    public void run() {
   
   

        //模拟一直有票
        while(true) {
   
     
            synchronized(obj) {
   
   //锁对像必须是同一把锁 
                if(tickets>0) {
   
   
                    //睡眠100毫秒
                    try {
   
   
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
   
   
                        e.printStackTrace();
                    }
                                        System.out.println(Thread.currentThread().getName()+"正在出售第"+(tickets--)+"张票");                    
                }
            }
        }
    } 
}

public static void main(String[] args) {
   
   
    //创建资源类对象
    SellTicket st = new SellTicket() ;

    //创建三个子线程
    Thread t1 = new Thread(st, "窗口1") ;
    Thread t2 = new Thread(st, "窗口2") ;
    Thread t3 = new Thread(st, "窗口3") ;


    //分别启动线程
    t1.start();
    t2.start();
    t3.start();
}
  • 加入了同步代码块,使得一个只能进去一个执行完后才会有另一个

8.4 同步方法

除了使用同步代码块还可以将synchronized关键字放在方法中,声明为同步方法;

synchronized 返回值类型 方法名称(形参列表){

​ //原子操作

}

  • 只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中。

  • 线程退出同步方法时,会释放相应的互斥锁标记。

  • 非静态的同步方法: 锁对象:this
  • 静态的同步方法的锁对象:当前这个类的字节码文件对象(反射有关系):任何类名.class
public class SellTicket implements Runnable {
   
   
    //100张票
    public static int tickets = 100;
    private int x = 0;

    public void run() {
   
   
        while(true) {
   
   //模仿一直有票
                sellTicket();
                x ++;
        }
    }

    public static synchronized void sellTicket(){
   
    //解决线程安全问题

        if(tickets>0) {
   
   
            //睡眠100毫秒
            try {
   
   
                Thread.sleep(100);
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"正在出售第"+(tickets--)+"张票");
        }
    }
}


public static void main(String[] args) {
   
   

    //创建资源类对象
    SellTicket st = new SellTicket() ;

    //创建显现出类对象
    Thread t1 = new Thread(st, "窗口1") ;
    Thread t2 = new Thread(st, "窗口2") ;
    Thread t3 = new Thread(st, "窗口3") ;

    //启动
    t1.start();
    t2.start();
    t3.start();
}
  • 效果和同步代码块一样

8.5 同步规则

  • 只有再调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记。
  • 如果用不包含同步代码块的方法,或普通方法时,不需要锁标记,可直接调用。
  • JDK中线程安全的类:StringBuffer、Vector、Hashtable都是由synchronized修饰。

凡是线程安全、效率低的都是添加了synchronized同步锁,同时只允许一个线程进行操作;效率高、线程不安全的没有synchronized同步锁,可以允许多个线程同时对一个内容进行操作。

//线程安全的类
StringBuffer sb = new StringBuffer();
Vector<String> v = new Vector<String>();
Hashtable<String, String> h = new Hashtable<String, String>();
//线程不安全
List<String> list1 = new ArrayList<String>();
//线程安全
List<String> list2 = Collections.synchronizedList(new ArrayList<String>());
  • 写操作(增、删、改)加锁,读操作不加锁

9. 线程通信

9.1 等待

必须在对obj(对象)加锁的同步代码块或方法中,在一个线程执行期间,调用了Obj.wait();会主动释放锁,同时进入到obj的等待队列中,进入无限等待直到唤醒。

  • public final void wait():在其他线程调用此对象的notify()方法之前一直处于等待状态
  • public final void wait(long timeout):在其他线程调用此对象的notify()方法之前或者超过指定的时间量前一直处于等待状态

9.2 唤醒

必须在obj加锁的同步代码块或方法中,从obj的waiting中释放一个或全部的线程;对自身没有影响。

  • Public final void notify():随机唤醒一个
  • Public final void notifyAll():唤醒所有的等待线程
public class TestDeadLock {
   
   
    public static void main(String[] args) throws InterruptedException {
   
   
        Object obj = new Object();
        MyThread t1 = new MyThread(obj);
        MyThread2 t2 = new MyThread2(obj);

        t1.start();
        t2.start();

        Thread.sleep(3000);
        synchronized (obj) {
   
   
            System.out.println(Thread.currentThread().getName()+"进入当前代码块");
            //obj.wait();//主动释放锁,并等待
            //obj.notify();//随机唤醒一个
            obj.notifyAll();//唤醒所有的线程
            System.out.println(Thread.currentThread().getName()+"退出同步代码块");
        }
    }
}

class MyThread extends Thread{
   
   
    Object obj;
    public  MyThread(Object obj){
   
   
        this.obj = obj;
    }
    public void run(){
   
   
        synchronized (obj) {
   
   
            System.out.println(Thread.currentThread().getName()+"进入同步代码块");
            try {
   
   
                obj.wait();//主动释放锁,并进入无限期等待
            } catch (InterruptedException e1) {
   
   
                e1.printStackTrace();
            }
            try {
   
   
                Thread.sleep(3000);
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"退出同步代码块");
        }
    }
}
class MyThread2 extends Thread{
   
   
    Object obj;
    public  MyThread2(Object obj){
   
   
        this.obj = obj;
    }
    public void run(){
   
   
        synchronized (obj) {
   
   
            System.out.println(Thread.currentThread().getName()+"进入同步代码块");
            //obj.notify();//在obj共享对象等待队列中,唤醒一个正在等待的线程
            try {
   
   
                obj.wait();//主动释放锁,并进入无限期等待
            } catch (InterruptedException e1) {
   
   
                e1.printStackTrace();
            }
            try {
   
   
                Thread.sleep(3000);
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"退出同步代码块");
        }
    }
}
  • 线程一和线程二进入后开始等待,直到main执行完毕唤醒后继续执行,如果不唤醒一直等待
执行结果:
Thread-0进入同步代码块
Thread-1进入同步代码块
main进入当前代码块
main退出同步代码块
Thread-1退出同步代码块
Thread-0退出同步代码块

10. 经典问题

10.1 死锁

  • 当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记,产生死锁。(也就是两个或者两个以上的线程在执行的过程中,因争夺资源产生的一种互相等待现象)
  • 一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,可能造成死锁
  • 使用synchronized同步代码块或者同步方法可以解决多线程的安全问题,也可以出现死锁
//在类中创建两把锁
public class MyDieLock {
   
   

    public static final Object objA = new Object();
    public static final Object objB = new Object();
}

//线程类
public class DieLock  extends Thread{
   
   

    private boolean flag;
    //有参
    public DieLock(boolean flag){
   
   
        this.flag = flag;
    }
    //重写run方法
    public void run() {
   
   
        if(flag){
   
   
            synchronized(MyDieLock.objA) {
   
   
                System.out.println("if objA");
                synchronized (MyDieLock.objB) {
   
   
                    System.out.println("if objB"); 
                }
            }
        }else {
   
   
            synchronized (MyDieLock.objB) {
   
   
                System.out.println("else objB");
                synchronized(MyDieLock.objA) {
   
   
                    System.out.println("else objA");
                }
            }
        }
    }
}
  • 理想状态: if objA if ObjB else objB else objA,但是运行出来不一定是这种情况

10.2 生产者消费者问题

           若干生产者在生产产品,这些产品将提供给若干消费者去消费,为使两者能够并发执行,在两者间设置一个能存储多个产品的缓冲区,生产者将生产好的放入缓冲区中,消费者取走进行消费,必须保持同步,不允许消费者在空时取走商品,也不允许生产者向一个满的缓冲区中放入商品。

public class TestProductCustomer {
   
   
    public static void main(String[] args) {
   
   
        //商品对象
        Shop shop = new Shop();
        Thread p = new Thread(new Product(shop),"生产者");
        Thread c = new Thread(new Customer(shop),"消费者");
        p.start();
        c.start();
    }
}
class Goods{
   
   
    private int id;
    public Goods(int id){
   
   
        this.id = id;
    }
    public int getId() {
   
   
        return id;
    }
    public void setId(int id) {
   
   
        this.id = id;
    }
}
class Shop {
   
   
    Goods goods;
    boolean flag;//商品是否充足
    //生产者调用存的方法
    public synchronized void saveGoods(Goods goods) throws InterruptedException{
   
   
        //判断是否充足
        if(flag == true){
   
   
            this.wait();//商品充足
        }
        //商品不充足
        System.out.println(Thread.currentThread().getName()+"生产并在商品里存放了"+goods.getId()+"件商品");
        this.goods = goods;
        flag = true;//已经有商品了,可买
        this.notifyAll();//唤醒
    }
    //调用取的方法
    public synchronized void buyGoods(Goods goods) throws InterruptedException{
   
   
        if(flag == false){
   
   
            this.wait();//消费者进入等待队列,等待生产者生产后唤醒
        }
        //正常购买
        System.out.println(Thread.currentThread().getName()+"购买了"+goods.getId()+"件商品");
        this.goods = null;
        flag = false;
        this.notifyAll();//唤醒生产商品
    }
}
//生产者
class Product implements Runnable{
   
   
    Shop shop;
    public Product (Shop shop){
   
   
        this.shop = shop;
    }
    public void run(){
   
   
        for(int i = 1; i<=10; i++){
   
   //通过循环,生产商品存到Shop中
            try {
   
   
                this.shop.saveGoods(new Goods(i));
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            }
        }
    }
}
//消费者
class Customer implements Runnable{
   
   
    Shop shop;
    public Customer (Shop shop){
   
   
        this.shop = shop;
    }
    public void run(){
   
   
        for(int i = 1; i<=10; i++){
   
   
            try {
   
   
                this.shop.buyGoods(new Goods(i));
            } catch (InterruptedException e) {
   
   
                e.printStackTrace();
            }
        }
    }
}
执行结果:
生产者生产并在商品里存放了1件商品
消费者购买了1件商品
生产者生产并在商品里存放了2件商品
消费者购买了2件商品
生产者生产并在商品里存放了3件商品
消费者购买了3件商品
生产者生产并在商品里存放了4件商品
消费者购买了4件商品
生产者生产并在商品里存放了5件商品
消费者购买了5件商品
生产者生产并在商品里存放了6件商品
消费者购买了6件商品
生产者生产并在商品里存放了7件商品
消费者购买了7件商品
生产者生产并在商品里存放了8件商品
消费者购买了8件商品
生产者生产并在商品里存放了9件商品
消费者购买了9件商品
生产者生产并在商品里存放了10件商品
消费者购买了10件商品
目录
相关文章
|
9天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
11天前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
11天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
11天前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
35 3
|
11天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
93 2
|
19天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
46 6
|
28天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
28天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
50 3
|
8月前
|
存储 安全 Java
深入理解Java并发编程:线程安全与锁机制
【5月更文挑战第31天】在Java并发编程中,线程安全和锁机制是两个核心概念。本文将深入探讨这两个概念,包括它们的定义、实现方式以及在实际开发中的应用。通过对线程安全和锁机制的深入理解,可以帮助我们更好地解决并发编程中的问题,提高程序的性能和稳定性。
|
5月前
|
存储 安全 Java
解锁Java并发编程奥秘:深入剖析Synchronized关键字的同步机制与实现原理,让多线程安全如磐石般稳固!
【8月更文挑战第4天】Java并发编程中,Synchronized关键字是确保多线程环境下数据一致性与线程安全的基础机制。它可通过修饰实例方法、静态方法或代码块来控制对共享资源的独占访问。Synchronized基于Java对象头中的监视器锁实现,通过MonitorEnter/MonitorExit指令管理锁的获取与释放。示例展示了如何使用Synchronized修饰方法以实现线程间的同步,避免数据竞争。掌握其原理对编写高效安全的多线程程序极为关键。
74 1