首页 > 编程语言 >Java多线程--Lesson03

Java多线程--Lesson03

时间:2023-09-27 10:25:35浏览次数:45  
标签:Java synchronized -- void class 线程 new 多线程 public

线程同步

概念:

线程同步指的是在多个线程操作同一资源时,需要通过线程排队和线程锁来约束这些线程,使得其可以对其资源完成同步

并发指的是同一时间段内,有多个线程去操作同一个资源文件

由于同一进程的多个线程共享一块空间资源,带来方便的同时也带来了冲突问题,为了保证数据在方法中被访问的唯一性,在访问时加入锁机制synchronized,当一个线程获得排他锁,独占资源,其它线程必须等待,释放锁后才可以使用

  • 一个线程拥有锁,则其它需要此锁的线程必须挂起等待
  • 在线程竞争环境激烈的情况下,加锁,释放锁会导致频繁的上下问文切换和调度延时,引起性能问题
  • 如果一个优先级高的线程的等待一个优先级低的释放锁,会导致优先级倒置,引起性能问题

三大线程不安全示例

第一种:多个线程去竞争同一资源

在资源有限的时候,很明显可以发现有一个问题那就是,在拿取最后一个资源的时候,都以为自己可以拿到,所以去操作了这个资源,就导致资源下溢为了负数

代码展示:

//不安全的买票,票数为 0或者 -1
public class Unsafe1 {
    public static void main(String[] args) {
        BuyTicket station = new BuyTicket();
        new Thread(station,"魈").start();
        new Thread(station,"胡桃").start();
        new Thread(station,"钟离").start();
    }
}
class BuyTicket implements Runnable {
private int tickets = 10;
Boolean flag = true;
    @Override
    public void run() {
        while (true){
            try {
                buy();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    public void buy() throws InterruptedException {
        if (tickets<0){
            flag=false;
            return;
        }
        Thread.sleep(10);
        System.out.println(Thread.currentThread().getName()+"拿到了第"+tickets--);
    }
}

 

//输出结果
钟离拿到了第10
魈拿到了第8
胡桃拿到了第9
魈拿到了第6
钟离拿到了第5
胡桃拿到了第7
魈拿到了第4
胡桃拿到了第3
钟离拿到了第4
钟离拿到了第2
胡桃拿到了第1
魈拿到了第1
钟离拿到了第0
魈拿到了第-1

第二种:多线程竞争单一资源

当多个线程取竞争单个资源的时候,会都拿到此资源的初始值,然后操作后结果会是很奇怪或者超出溢值的,这是因为每个线程都有独属于自己的运算内存,它们相互分开互不干扰

代码展示:

package Multihead;

public class Unsafe2 {
    public static void main(String[] args) {
        Account account = new Account(100, "结婚基金");
        Drowing you = new Drowing(account, 50, "你");
        Drowing me = new Drowing(account, 70, "我");
        you.start();
        me.start();
    }
}

class Account{
    int money;
    String name;

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
class Drowing extends Thread{
    Account account;
    int drawingMoney;
    int nowMoney;
    String name;

    public Drowing( Account account, int drawingMoney, String name) {
        this.account = account;
        this.drawingMoney = drawingMoney;
        this.name = name;
    }

    @Override
    public void run() {
        if (account.money-drawingMoney<0){
            System.out.println(this.getName()+"钱不够了");
            return;
        }
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //卡内余额 - 取的钱
        account.money=account.money-drawingMoney;
        nowMoney=nowMoney+drawingMoney;
        System.out.println(account.getName()+"余额为:"+account.money);
        System.out.println(this.getName()+"手里的钱:"+nowMoney);
    }


}
//运行结果
结婚基金余额为:-20
结婚基金余额为:-20
Thread-0手里的钱:50
Thread-1手里的钱:70

  可以看到,两个都对初始值100,操作了,导致余额本来是取不出来的,但是他们的操作中,先拿到数据的还没写回,后一个线程就又开始操作了

第三种:多个线程顺序写资源时会发生覆盖现象

这种情况发生在当我们的资源是连续的时候,当线程拿到同一资源时,有的先写回,有的后写回,由于线程的算法一致,这就导致后写回的数据覆盖了先写回的数据

代码展示:

public class Unsafe3 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        System.out.println(list.size());
    }
}
// 输出结果:999 比预计的少了一个

 

这个是数据量越大,越明显,被覆盖的越多

线程同步操作

在Java中我们可以使用privtae修饰符来修饰属性变为不可访问,相同的也可以使用针对方法的一套机制,这套机制就是synchronized关键字,它有两种用法:synchronized方法和synchronized块

同步方法:

public synchronized void method()
{
}

synchronized控制每个对象的访问,每个对象都有一把自己的锁,每个synchronized方法都必须要获得锁以后才可以执行,否则就会阻塞

并且每个synchronized方法执行的时候都会独占一把锁,直到方法结束以后才会释放锁,后面的线程才能获得锁

缺陷:如果一个很大的synchronized方法获得锁会非常影响效率

代码示例:

    public synchronized void buy() throws InterruptedException {
        if (tickets<=0){
            flag=false;
            return;
        }
        Thread.sleep(10);
        System.out.println(Thread.currentThread().getName()+"拿到了第"+tickets--);
    }

加了synchronized关键字的方法会监视此方法中的对象,对象中所有的对象都只能在线程有锁的情况下执行

同步块

同步块指监视此区域中的某个对象。

 同步块:

synchronized(obj){ 
//代码块
 }

 

obj:称之为同步监视

obj可以是任何对象,但是推荐使用共享资源作为同步监视器

同步方法中无需指定监视对象,因为同步方法的同步监视器就是 this ,就是这个对象本身,或者是class

同步监视器的执行过程

  • 第一个线程访问,锁定同步监视器,执行其中代码
  • 第二个线程访问,发现同步监视器被锁定,无法访问
  • 第一个线程访问完毕,解锁同步监视器
  • 第二个线程访问,发现同步监视器没有锁,然后锁定并访问

代码展示:

  @Override
    public void run() {
        //此方法是操作的代码,并不是account的拥有者
        synchronized (account){  
            //使用synchronized同步块,绑定实际要操作的对象account,这里是也就是账户
        if (account.money-drawingMoney<0){
            System.out.println(this.getName()+"钱不够了");
            return;
        }
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //卡内余额 - 取的钱
        account.money=account.money-drawingMoney;
        nowMoney=nowMoney+drawingMoney;
        System.out.println(account.getName()+"余额为:"+account.money);
        System.out.println(this.getName()+"手里的钱:"+nowMoney);
    }
}

使用同步块可以监视唯一对象,它和同步方法都是可以实现同步上锁,只是上锁的对象一个是自定义,一个是默认this

 死锁

多个线程各自占有一些共用资源,并且互相等待其它线程占有的资源释放后才能运行。而导致两个或多个线程在处于等待资源的状态,都停止的状态

某一个同步块需要同时拥有两个以上的对象的锁时,可能发生“死锁”问题

代码展示:

package Multihead;

public class DeadLock {
    public static void main(String[] args) {
        HeBao hu = new HeBao("胡桃");
        HeBao xiao = new HeBao("魈");
        hu.start();
        xiao.start();
    }
}
//护摩之杖
class HuMo{

}
//270专用圣遗物
class ShenYiWu{

}
//核爆手法
class HeBao extends Thread{
//使用static关键字修饰,保持全局唯一 static HuMo huMo =new HuMo(); static ShenYiWu shenYiWu=new ShenYiWu(); String name;//装备人物 public HeBao(String name) { this.name = name; } @Override public void run() { try { StartHeBao(); } catch (InterruptedException e) { throw new RuntimeException(e); } } public void StartHeBao() throws InterruptedException { if (name.equals("魈")){ synchronized (huMo){ System.out.println(name+"获得护摩之杖"); Thread.sleep(100); synchronized (shenYiWu){ System.out.println(name+"获得圣遗物开始核爆"); } } }else { synchronized (shenYiWu){ System.out.println(name+"获得圣遗物"); Thread.sleep(100); synchronized (huMo){ System.out.println(name+"获得护摩之杖开始核爆"); } } } } }

 

如上代码:有两个对象,护摩和圣遗物,当每个人物要同时拿到这两个对象才可以进行核爆,不然就不能核爆,这两个对象全局唯一

由于两个线程同时开启,所以刚开始都拿到了一个对象,但是要两个对象同时拥有才可以核爆,所以都在等待对方先核爆完卸下对象,所以都核爆不了,这就是死锁

产生死锁的四个必要条件:

  1. 互斥条件:一个资源每次只能被一个进程使用
  2. 请求与保持条件:一个进程因请求而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:进程已获得资源,在未使用完前,不能强行剥夺
  4. 循环等待条件:若干进程形成一种首尾相连的循环等待资源关系

Lock锁(自定义锁)

从jdk5.0开始,Java提供了强大的线程同步机制------通过显式定义同步锁对象实现同步,同步锁使用Lock对象来充当

java.util.concurrent.locks.Lock接口控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应该先获得Lock锁

ReentrantLock实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrentLock,可以显式加锁,释放锁

代码展示:

 public  void buy() throws InterruptedException {
        try {
            lock.lock();
            if (tickets<=0){
                flag=false;
                return;
            }
            Thread.sleep(10);
            System.out.println(Thread.currentThread().getName()+"拿到了第"+tickets--);
        }finally {
            lock.unlock();
        }
    }

 

加锁和释放锁的语句一般写在try-catch语句中,上锁的语句都写在try{ }代码块,释放锁写在finally{ }代码块

synchronized和Lock对比:

Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放

Lock只有代码锁块,synchronized有代码块锁和方法锁

使用Lock锁,JVM将花费少量的时间来调度线程,性能更好,并且具有更好的拓展性

优先使用顺序:

Lock >  同步代码块 > 同步方法

线程通信

线程通信是用于解决多线程的生产者与消费者的问题,当不同的线程之间所履行的职责不同的时候,有的线程负责生产,有的负责消费,它们不能协调很容易导致生产过多,或没有资料的问题

线程通信使得两个或多个线程可以相互交流,协同生产

Java提供了几个线程通信的问题

  • wait():表示线程一直等待,直到其它线程通知,与sleep不同,会释放锁
  • wait(long timeout):指定等待的毫秒数
  • notify():唤醒一个处于等待的线程
  • notifyAll():唤醒同一个对象上所有使用wait()方法的线程,优先级别高的线程优先调度

注意:均是Object类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常

解决线程通信的方法

第一种:管程法

  • 生产者:负责生产数据的模块
  • 消费者:负责处理数据的模块
  • 缓冲区:消费者不能直接使用生产者的数据,它们之间有一个缓冲区

生产者将生产的数据放在缓冲区里,消费者从缓冲区中拿

代码展示:

package Multihead;

public class TestPC {
    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Productor(container).start();
        new Consumer(container).start();
    }
}

//生产者
class Productor extends Thread{
    SynContainer synContainer;

    public Productor(SynContainer synContainer) {
        this.synContainer = synContainer;
    }
    
    //生产产品
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            synContainer.push(new Chicken(i));
            System.out.println("生产了"+i+"只鸡");
        }
    }
}
//消费者
class Consumer extends Thread{
    SynContainer synContainer;

    public Consumer(SynContainer synContainer) {
        this.synContainer = synContainer;
    }
    //消费产品
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费了----"+synContainer.pop().id+"只鸡");
        }
    }
}
//产品
class Chicken{
    int id;

    public Chicken(int id) {
        this.id = id;
    }
}
class SynContainer{
     Chicken[] chickens= new Chicken[10];

     int count =0;
     //生产产品到 10个,否则不能消费
     public synchronized void push(Chicken chicken){
         if (count== chickens.length){
             try {
                 this.wait();
             } catch (InterruptedException e) {
                 throw new RuntimeException(e);
             }
         }
         chickens[count]=chicken;
         count++;
         this.notifyAll();
     }
     //消费产品,当没有产品时,通知生产者生产
     public synchronized Chicken pop(){
         if (count==0){
             //等待
             try {
                 this.wait();
             } catch (InterruptedException e) {
                 throw new RuntimeException(e);
             }
         }
         count--;
         Chicken chicken=chickens[count];
         this.notifyAll();
         return chicken;
     }
}

 

第二种:信号灯法

设置一个标志位,利用标志位控制线程的启动和停止

代码展示:

package Multihead;

public class TestPC2 {
    public static void main(String[] args) {
      new go().start();
      new go().start();
    }
}
class go extends Thread{
    Boolean flag = false;
    int count=0;
    public go() {
    }
    @Override
    public void run() {

        try {
            for (count = 0; count < 10; count++) {
                Tv tv = new Tv(flag);
                flag=tv.keep();
            }

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
class Tv{
    Boolean flag;

    public Tv(Boolean flag) {
        this.flag = flag;
    }

    public synchronized Boolean keep() throws InterruptedException {
        Thread.sleep(200);
        if (flag){
            System.out.println("boy拿到了");
        }else {
            System.out.println("girl拿到了");
        }
        flag = !flag;
        return flag;
    }
}

信号灯法,旨在利用标志位控制一些线程工作,然后另一些线程又被反向标志位控制

 线程池

背景:经常创建和销毁线程,使用量特别大的资源,比如并发情况下的线程,对性能影响很大

思路提前创建多个线程池,放入线程池中,使用时直接获取,使用完后放回线程池。可以频繁的创建和销毁,实现重复利用,类似生活中的交通工具

好处:

  • 提高响应速度(减少了创建新线程的时间)
  • 降低了资源消耗(重复利用线程中的线程,不需要每次都创建)
  • 便于线程管理
  1. corePoolSize:核心池的大小
  2. maximumPoolSize:最大线程数
  3. keepAliveTime:线程没有任务时最多保持多长时间会终止

JDK5.0提供了线程相关的API:ExecutorService和Executors

ExecutorService:真正的线程接口,常见的子类,TheadPoolExecutor

  • void execute(Runnable command):执行任务命令,没有返回值,一般用来执行Runnable
  • <T> Future<T> submit ( callable<T > task):执行任务,有返回值,一般又来执行callable
  • void shutdown():关闭链接池

Executors:工具类,线程池的工厂类,用于创建并返回不同类型的线程池

代码展示:

package Multihead;

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

public class TestPool {
    public static void main(String[] args) {
        //创建线程池的大小
        ExecutorService service = Executors.newFixedThreadPool(3);
        //从线程池拿取线程执行
        service.execute(new myThead());
        service.execute(new myThead());
        service.execute(new myThead());
        service.execute(new myThead());
        //使用完关闭线程
        service.shutdown();
    }
}

class myThead implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

 

如上:在创建线程池时,只创建了三条线程,但是执行的时候却有4个任务需要使用,所以最后一个任务肯定要其它线程执行完后,才会被执行

//输出结果
pool-1-thread-3
pool-1-thread-1
pool-1-thread-2
pool-1-thread-3

 

如上,线程3先执行完,所以他多执行了一个任务

回顾线程创建的三种方式:

package Multihead;

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

public class TheadNew {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 继承Thread类的运行方式
        new Thead1().start();
        // 实现Runnable接口的运行方式
        new Thread(new Thead2()).start();
        // 实现callable接口的运行方式
        FutureTask<Integer> task = new FutureTask<>(new Thead3());
        new Thread(task).start();
        Integer i = task.get();
        System.out.println(i);
    }
}
// 继承Thead类
class Thead1 extends Thread{
    @Override
    public void run() {
        System.out.println("继承Thead类");
    }
}

//实现Runnable接口
class Thead2 implements Runnable{
    @Override
    public void run() {
        System.out.println("实现Runnable接口");
    }
}
//实现callable接口
class Thead3 implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        System.out.println("实现callable接口");
        return 100;
    }
}

 

标签:Java,synchronized,--,void,class,线程,new,多线程,public
From: https://www.cnblogs.com/5ran2yl/p/17729763.html

相关文章

  • Linux 6.5+ 带来了一些针对 AMD Ryzen Z1 Extreme 的性能/改进
    导读最近发布的 Linux 6.5内核默认启用了AMDP-StateEPP,用于现代Ryzen系统,而不再使用通用的ACPICPUFreq驱动程序。在各种工作负载下运行Linux6.5(或更新版本)可以提高性能和/或能效。对于移动端的影响,我最近在Linux6.3、6.4、6.5和6.6Git内核上对ASUSROGA......
  • 库函数 | C++17 std::filesystem文件系统 用法指北
    本文将针对常用的场景,对std::filesystem的使用逐一进行验证:判断文件夹是否存在创建单层目录逐级创建多层目录创建多级目录当前文件路径创建文件"from.dat"获取相对于base的绝对路径文件拷贝移动文件或重命名创建文件“example.dat”获取文件大小获取文件最后修改......
  • CH32V208以太网IAP修改用户区大小注意事项
    CH32v208以太网IAP修改用户区大小注意事项    CH32v208的以太网IAP程序中将FLASH分为3个区域,boot区40k、用户区44k和备份区44k,通过以太网接收到对端设备发来的以太网数据存到备份区中,之后复制到用户区,然后校验跳转,详细使用方法可以看一下例程中附带的使用教程。   ......
  • Idea配置热部署插件Jrebel
    Idea配置热部署插件Jrebel先从pulgins下载插件jrebelidea内配置jrebel百度下载reverseproxy_windows_amd64.exe,下载后直接打开。会显示一个终端控制台。之后不用管挂那做下一步(这个界面从Idea的help最下面有JRebel-Activation打开)teamURL第一行http://localhost:8888/......
  • 通过OAuth 2.0开放授权实现微信扫码登录第三方平台
    https://www.bilibili.com/video/BV1vh4y187an/?spm_id_from=333.1007.tianma.2-2-5.click&vd_source=0d7b1712ce42c1a2fa54bb4e1d601d78  ......
  • Linux命令杂记
    可能不是很有序,但都是实用命令,不会面面俱到,多了容易记不住find:查找文件命令。用法:find路径选项搜索内容递归搜索当前目录下的stdio.h文件gcc:编译。流程常用选项......
  • HarmonyOS线性容器特性及使用场景
     线性容器实现能按顺序访问的数据结构,其底层主要通过数组实现,包括ArrayList、Vector、List、LinkedList、Deque、Queue、Stack七种。线性容器,充分考虑了数据访问的速度,运行时(Runtime)通过一条字节码指令就可以完成增、删、改、查等操作。ArrayListArrayList即动态数组,可用来......
  • centos7 网卡配置文件解读
    借的图 另外,/etc/resolv.conf 是DNS客户机配置文件,用于设置DNS服务器的IP地址及DNS域名,还包含了主机的域名搜索顺序它的格式很简单,每行以一个关键字开头,后接一个或多个由空格隔开的参数 nameserver 8.8.8.8借鉴的:https://blog.csdn.net/lcr_happy/article/details/......
  • python range中的步长必须是整数 numpy则可以是小数
    >>>foriiinrange(1,10,0.1): print(ii)Traceback(mostrecentcalllast):File"<pyshell#4>",line1,in<module>foriiinrange(1,10,0.1):TypeError:'float'objectcannotbeinterpretedasaninteger>>......
  • SE C# 添加 事件监听 --选择对象切换监听
    usingSystem;usingSystem.Collections.Generic;usingSystem.ComponentModel;usingSystem.Data;usingSystem.Drawing;usingSystem.Linq;usingSystem.Text;usingSystem.Windows.Forms;usingSystem.Runtime.InteropServices.ComTypes;usingSystem.Runtime.InteropServi......