JAVA基础:多线程 (学习笔记)

多线程

一,什么是线程?

  • 程序:为完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码
  • 进程:程序的一次执行过程。

正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。 (进程是动态的)是一个动的过程 ,进程的生命周期 : 有它自身的产生、存在和消亡的过程

  • 线程:是进程的进一步细化, 是一个程序内部的一条执行路径。

若一个进程同一时间并行执行多个线程,就是支持多线程的。

1.并行和并发

  • 并行:多个CPU同时执行多个任务
  • 并发:一个CPU"同时"执行多个任务(采用时间片切换)

二,创建线程的三种方式

1,继承Thread类

  1. 这个run方法不能直接调用,直接调用就会被当做一个普通方法,要用 . start() ,启动线程。
  2. 开辟道路的代码必须在前面!!
入门例子
java 复制代码
//运行的类
public static void main(String[] args) {

   /*   DomeThread dt = new DomeThread();
        dt.start();*/

        new DomeThread().start();//开了一条新的路

        for (int i = 0; i < 100; i++) {
            System.out.println("main()" + i);
        }

    }//穿插运行
}
================================================================================
//准备
public class DomeThread extends Thread {//继承线程类  重写run()

    @Override
    public void run() {//另外一条道的执行代码
        show();
    }

    public void show() {
        for (int i = 0; i < 100; i++) {
            System.out.println("DomeThread_show()" + i);
        }
    }
}
三个窗口售卖火车票----三种方式的例题,也是线程安全问题的例题
java 复制代码
//运行
 /**
     * 用继承方式的并行售票
     *
     * @param args
     */

    public static void main2(String[] args) {//main方法---主线程
        //假设有三个窗口同时买票
        //没有构造方法命名会报错
        TicketThread tt1 = new TicketThread("窗口1");//命名方法1
        TicketThread tt2 = new TicketThread();
        tt2.setName("窗口2");//命名方法2
        TicketThread tt3 = new TicketThread("窗口3");
        tt1.start();
        tt2.start();
        tt3.start();

    }
===========================================================================================
//准备

public class TicketThread extends Thread{


    static int count=10;//静态--有一个对象对他进行改变,其他对象再使用时就是修改过的

    @Override
    public void run() {
        while (count>0) {
            count--;
            System.out.println(this.getName()+"卖出一张票,还剩"+count+"张");
        }
    }

    public TicketThread() {
    }

    public TicketThread(String name) {
        super(name);
    }
}
缺点:
  1. 没有返回值
  2. 不能抛出异常

接 三,1------同步代码块 (重写 run() )

java 复制代码
 @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            synchronized (TicketThread.class) {//同步代码块    同步关键字(锁子)
                if (count > 0) {
                    count--;
                    System.out.println(this.getName() + "卖出一张票,还剩" + count + "张");
                }
            }
        }
    }
//TicketThread.class  或者 this.getClass

2,实现Runnable接口

Thread类实现了Runnable接口

java 复制代码
//运行 
 public static void main3(String[] args) {
        TicketRunnable tr = new TicketRunnable();
        Thread thread1 = new Thread(tr, "窗口1");
        Thread thread2 = new Thread(tr, "窗口2");
        Thread thread3 = new Thread(tr, "窗口2");
        thread1.start();
        thread2.start();
        thread3.start();
    }

============================================================================
//准备

public class TicketRunnable implements Runnable {
    int count = 10;


    @Override
    public void run() {
        while (count > 0) {
            count--;
            System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩" + count + "张");
        }//Thread.currentThread().getName()---获得正在运行的线程的名字
    }

}
缺点
  1. 没有返回值
  2. 不能抛出异常

接 三,2----- 同步方法 (重写run() , 新增一个synchronized同步方法)

java 复制代码
package thread;

public class TicketRunnable implements Runnable {
    int count = 10;


    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            buyTicket();
        }
    }
    public synchronized  void buyTicket(){//默认锁 this对象
        if (count > 0) {
            count--;
            System.out.println(Thread.currentThread().getName()+ "卖出一张票,还剩" + count + "张");
        }
    }
}

补充---休眠

java 复制代码
 @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            buyTicket();

           //休眠,如果运行时一直是一个窗口,可以用这个
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        }
    }

接 三,3----Lock锁 (重写run() , 定义锁)

java 复制代码
package thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TicketRunnable implements Runnable {
    int count = 10;

    Lock lock=new ReentrantLock(); //多态  接口=实现类  可以使用不同的实现类
    @Override
    public void run() {

        for (int i = 0; i <= 100; i++) {
            lock.lock();
            try {
                if (count > 0) {
                    count--;
                    System.out.println(Thread.currentThread().getName()+ "卖出一张票,还剩" + count + "张");
                }
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }finally {
                lock.unlock();
            }


        }
    }
  
}

3,实现Callable接口

好处:
  1. 有返回值
  2. 能抛出异常
缺点:
  1. 线程创建比较麻烦
java 复制代码
//运行 
/**
     * 实现Callable接口的并行售票
     */
 public static void main(String[] args) {
        TicketCall tc=new TicketCall();

        FutureTask ft=new FutureTask(tc);
        Thread t1=new Thread(ft,"窗口1");//存在默认名,如果你不命名,程序会显示Thread-0
        FutureTask ft1=new FutureTask(tc);
        Thread t2=new Thread(ft1,"窗口2");
        FutureTask ft2=new FutureTask(tc);
        Thread t3=new Thread(ft2,"窗口3");
        t1.start();
        t2.start();
        t3.start();

//获取线程得到的返回值:
        Object obj = ft.get();
        System.out.println(obj);

    }
======================================================================
//准备

package thread;

import java.util.concurrent.Callable;

public class TicketCall implements Callable {

    int count = 10;

    @Override
    public Object call() throws Exception {
        while (count > 0) {
            count--;
            System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩" + count + "张");
        }//Thread.currentThread().getName()---获得正在运行的线程的名字
        return null;
    }
}
补充
  1. 实现Callable接口,可以不带泛型,如果不带泛型,那么call方式的返回值就是Object类型

    java 复制代码
    public class TicketCall implements Callable<Integer> {
  2. 如果带泛型,那么call的返回值就是泛型对应的类型

  3. 从call方法看到:方法有返回值,可以跑出异常

4,线程的生命周期

四,线程的常用方法

  • start() : 启动当前线程,表面上调用start方法,实际在调用线程里面的run方法 【例题中有】
  • run() : 线程类 继承 Thread类 或者 实现Runnable接口的时候,都要重新实现这个run方法,run方法里面是线程要执行的内容 【例题中有】
  • currentThread :Thread类中一个静态方法:获取当前正在执行的线程 【Runnable例中】
  • setName : 设置线程名字 【Thread例中】
  • getName 读取线程名字 【例题中有】
  • 通过调用**interrupt()**方法来中断其阻塞状态
  • 设置优先级
  1. 同优先级别的线程,采取的策略就是先到先服务,使用时间片策略
  2. 如果优先级别高,被CPU调度的概率就高
  3. 级别:1-10 默认的级别为5
  4. 线程的优先级是在创建线程时设置的,在创建线程后的任何时候都可以重新设置
java 复制代码
//运行
class Test{
    //这是main方法,程序的入口
    public static void main(String[] args) {
        //创建两个子线程,让这两个子线程争抢资源:
        TestThread01 t1 = new TestThread01();
        t1.setPriority(10);//优先级别高
        t1.start();
        TestThread02 t2 = new TestThread02();
        t2.setPriority(1);//优先级别低
        t2.start();
    }
}
=====================================================================
//准备
public class TestThread01 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(i);
        }
    }
}
-----------------------------------------------------------
class TestThread02 extends Thread{
    @Override
    public void run() {
        for (int i = 20; i <= 30 ; i++) {
            System.out.println(i);
        }
    }
}
  • join() : 当一个线程调用了join方法,这个线程就会先被执行,它执行结束以后才可以去执行其余的线程。 注意:必须先start,再join才有效。
java 复制代码
//测试
class Test{
    //这是main方法,程序的入口
    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 100 ; i++) {
            System.out.println("main-----"+i);
            if(i == 6){
                //创建子线程:
                TestThread tt = new TestThread("子线程");
                tt.start();
                tt.join();//"半路杀出个程咬金"
            }
        }
    }
}
====================================================
//准备
public class TestThread extends Thread {
    public TestThread(String name){
        super(name);
    }
    @Override
    public void run() {
        for (int i = 1; i <= 10 ; i++) {
            System.out.println(this.getName()+"----"+i);
        }
    }
}
  • sleep : 人为的制造阻塞事件
java 复制代码
public class Test01 {
    //这是main方法,程序的入口
    public static void main(String[] args) {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("00000000000000");
    }
}

案例:完成秒表功能

java 复制代码
public class Test02 {
    //这是main方法,程序的入口
    public static void main(String[] args) {
        //2.定义一个时间格式:
        DateFormat df = new SimpleDateFormat("HH:mm:ss");
        while(true){
            //1.获取当前时间:
            Date d = new Date();
            //3.按照上面定义的格式将Date类型转为指定格式的字符串:
            System.out.println(df.format(d));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • setDaemon设置伴随线程
  1. 将子线程设置为主线程的伴随线程,主线程停止的时候,子线程也不要继续执行了
  2. 案例:皇上 --》驾崩 ---》妃子陪葬 【将指定的线程设置成后台线程】
java 复制代码
//运行
class Test{
    //这是main方法,程序的入口
    public static void main(String[] args) {
        //创建并启动子线程:
        TestThread tt = new TestThread();
        tt.setDaemon(true);//设置伴随线程  注意:先设置,再启动
        tt.start();
        //主线程中还要输出1-10的数字:
        for (int i = 1; i <= 10 ; i++) {
            System.out.println("main---"+i);
        }
    }
}
=========================================================
//准备
public class TestThread extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 1000 ; i++) {
            System.out.println("子线程----"+i);
        }
    }
}
  • yield方法----的作用是让当前线程放弃CPU时间片,进入和可运行状态,与其他等待的可运行状态线程竞争CPU时间片。它并不会直接提升线程的优先级,而是让当前线程主动放弃执行权,从而让其他线程有机会运行‌

五,线程安全问题

1,同步代码块 //代码见 上面 --接 三,1

java 复制代码
 synchronized (TicketThread.class)
总结一

----认识同步监视器(锁子) ----- synchronized(同步监视器){ }

  1. 必须是引用数据类型,不能是基本数据类型
  2. 也可以创建一个专门的同步监视器,没有任何业务含义
  3. 一般使用共享资源做同步监视器即可
  4. 在同步代码块中不能改变同步监视器对象的引用
  5. 尽量不要String和包装类Integer做同步监视器
  6. 建议使用final修饰同步监视器
总结二

----同步代码块的执行过程

  1. 第一个线程来到同步代码块,发现同步监视器open状态,需要close,然后执行其中的代码。
  2. 第一个线程执行过程中,发生了线程切换(阻塞 就绪),第一个线程失去了cpu,但是没有开锁open。
  3. 第二个线程获取了cpu,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态。
  4. 第一个线程再次获取CPU,接着执行后续的代码;同步代码块执行完毕,释放锁open。
  5. 第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,拿到锁并且上锁,由阻塞状态进入就绪状态,再进入运行状态,重复第一个线程的处理过程(加锁)。
  6. 强调:同步代码块中能发生CPU的切换吗?能!!! 但是后续的被执行的线程也无法执行同步代码块(因为锁仍旧close) 。
总结三
  1. 多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块
  2. 多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块, 但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块

2,同步方法 //代码见 上面 --接 三,2

java 复制代码
 public synchronized  void buyTicket(){  }
总结一
  1. 多线程在争抢资源,就要实现线程的同步(就要进行加锁,并且这个锁必须是共享的,必须是唯一的。
  2. 咱们的锁一般都是引用数据类型的。
  3. 目的:解决了线程安全问题。
总结二

--- 关于同步方法

  1. 不要将run()定义为同步方法
  2. 非静态同步方法的同步监视器是this
  3. 静态同步方法的同步监视器是 类名.class 字节码信息对象
  4. 同步代码块的效率要高于同步方法 ------------原因:同步方法是将线程挡在了方法的外部,而同步代码块锁将线程挡在了代码块的外部,但是却是方法的内部
  5. 同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法;同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块

3,Lock锁 //代码见 上面 --接 三,3

java 复制代码
 Lock lock=new ReentrantLock(); //多态  接口=实现类  可以使用不同的实现类
    。。。。。。。。
 lock.lock();//打开锁
    。。。。。。。
 lock.unlock();//关闭锁:--->即使有异常,这个锁也可以得到释放
Lock和synchronized的区别
  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. .使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
  1. Lock----同步代码块(已经进入了方法体,分配了相应资源)----同步方法(在方法体之外)
相关推荐
星河梦瑾30 分钟前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富34 分钟前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想36 分钟前
JMeter 使用详解
java·jmeter
言、雲38 分钟前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
OopspoO43 分钟前
qcow2镜像大小压缩
学习·性能优化
TT哇1 小时前
【数据结构练习题】链表与LinkedList
java·数据结构·链表
A懿轩A1 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
Yvemil71 小时前
《开启微服务之旅:Spring Boot 从入门到实践》(三)
java
Anna。。1 小时前
Java入门2-idea 第五章:IO流(java.io包中)
java·开发语言·intellij-idea
居居飒1 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin