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件商品
目录
相关文章
|
4天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
23 9
|
7天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
4天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
7天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
21 3
|
6天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
7天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
17 1
|
7天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
8天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
33 1
|
3月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
115 1
|
6月前
|
设计模式 监控 Java
Java多线程基础-11:工厂模式及代码案例之线程池(一)
本文介绍了Java并发框架中的线程池工具,特别是`java.util.concurrent`包中的`Executors`和`ThreadPoolExecutor`类。线程池通过预先创建并管理一组线程,可以提高多线程任务的效率和响应速度,减少线程创建和销毁的开销。
195 2