线程相关(java)

一、线程概念

进程与线程的区别与关联:

  1. 进程是程序向操作系统申请资源的基本单位。线程是进程中可独立执行的最小单位。
  2. 一个进程可以包含多个线程,同一个进程中的所有线程共享该进程中的资源如内存空间、文件句柄等
  3. 线程所要完成的计算被称为任务,特定的线程总是执行着特定的任务。

什么是多线程编程

函数式编程中的函数是基本抽象单位,面向对象编程中的类(class)是基本的抽象单位。类似,多线程编程就是以线程为基本抽象单位的一种编程范式。

线程状态图:在这里插入图片描述

线程安全与线程不安全

定义

当一个类在多线程的情况下不是运行结果与期望的结果不一致时,我们说该类是线程不安全的,一个类不是线程安全的,我们就说它在多线程的环境下直接使用是存在线程安全问题,线程安全问题主要体现在三个方面:原子性、可见性、有序性。

原子性
原子(Atomic)的字面意思是不可分割的。对于涉及共享变量访问的操作,若该操作从其执行线程以外的任何线程来看都是不可分割的,那么该操作就是原子操作,相应地我们称该操作具有原子性。(原子操作+原子操作!=原子操作),复合操作不一定具有原子性java语言规范决定,除了double,long类型的变量读写不具备原子性,其他的基本类型都是原子性的。

可见性
1.在多线程环境下,一个线程对某个共享变量进行更新后,后续访问该变量的线程可能无法立刻读取到这个更新的结果,甚至永远也无法读取到这个更新结果。这就是线程安全的另外一个表现形式:可见性(Visibility).
2.java中如何保证变量的可见性,volatile关键字可以提示JIT编译器被修饰的变量可能被多个线程共享,以阻止JIT编译器做出可能导致程序运行不正常的优化,另一个作用就是读取一个volatile关键字修饰的变量会使对应的处理器刷新处理器缓存的动作,写一个volatile关键字修饰的变量会使相应的处理器冲刷处理器缓存的动作,从而保证了可见性。
3.从保证线程安全的角度来看,光保证原子性可能是不够的,有时候还要同时保证可见性,可见性的原子性同时得以保证才能够保证一个线程能够“正确”的看到其他线程对共享变量所做的贡献。
有序性
1.有序性(ordering)指在什么情况下一个处理器上运行一个线程所执行的内存访问操作在另外一个处理器上运行的其他线程看来是乱序的。所谓乱序,是指内存访问操作的顺序看起来像是发生了变化。
2.重排序的概念:顺序结构是结构化编程中的一种基本结构,它表示我们希望某个操作先于另外一个操作执行得以执行。另外两个操作即便是可以使用任意一种顺序执行,但是反映在代码上面这两个操作总是有先后关系的。但是在多核处理器的环境下,这种操作执行顺序可能是没有保障的:编译器可能改变两个操作的先后顺序;处理器可能不是完全按照程序的目标代码所指定的;另外一个处理器上执行多个操作,从其他处理器的角度来看其顺序可能与目标代码所指定的顺序不一致,这种现象叫做重排序(ReOrdering).

上下文切换

1、线程上下文切换就是为了从一个线程的运行环境切换到另外一个线程的运行环境。
2、在单处理器的机器上也可以同时运行多个线程,单处理器上面通过时间片的概念来同时运行多个线程,每个独立的线程会从处理器获取到一定的时间片来运行,当时间片耗完或者线程自己需要等待而终止,那么就需要上下文切换来切换到另外一个线程的运行环境,另一个线程获取到时间片进而开始执行。    
3、当一个线程被剥夺处理器的使用权而被暂停运行被称为切出(Switch out);一个线程被操作系统选中占用处理器开始或者继续其运行就被称为切入(Switch in)。

资源的竞争与调度

Java线程同步机制

简介

  1. 从应用程序的角度来看,线程安全问题的产生是由于多线程应用程序缺乏某种东西------线程同步机制。线程同步机制是一套用于协调线程间数据访问(Data
    access)及活动(Activity)的机制,该机制用于保障线程安全以及实现这些线程的共同目标
  2. 从广义上来说,Java平台提供的线程同步机制包括锁、volatile关键字、final关键字、static关键字以及一些相应的API,如Object.wait()以及Object.notify()等。

锁的概述

线程安全问题的产生前提是多个线程并发访问共享变量、共享资源。那么如何解决共享变量的访问问题呢

我们很容易想到一种保障线程安全的解决方式----将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据只能被一个相乘访问,该线程访问结束后其他线程才能对其进行访问。锁(LOCK)就是利用这种思路以保证线程安全的线程同步机制。

定义
  1. 一个线程在访问共享数据的时候必须申请相应的锁(许可证),线程的的这个动作叫做锁的获取(AAcquire)。一个线程获得某个锁(持有许可证),我们称该线程为相应锁的持有线程,一个锁一次只能被一个线程持有,锁的持有线程可以对该锁所保护的共享数据进行访问,访问结束后该线程必须释放(Release)锁。
  2. 锁的持有线程在其获取锁之后到释放锁之前这段时间内所执行的代码被称为临界区(Critical Section)。因此共享数据只允许在临界区内访问,临界区一次只能被一个线程执行。
  3. 锁具有排它性(Exclusive),即一个锁只能被一个线程持有,因此这种锁叫做排它锁或者互斥锁。
  4. Java平台中的锁包括内部锁(Intrinsic Lock)和显式锁(Explicit Lock)。内部锁是通过synchronized关键字实现,显式锁式通过java.concurrent.locks.lock的实现类(如java.concurrent.locks.ReentrantLock类)实现的。
关于锁的几个概念

1、可重入性(Reentrancy)描述这样一个问题,一个线程在持有一个锁的时候是否能再次(或者多次申请该锁)。如果还能申请那么就是可重入性的锁,反之则是不可重入的。
2、 锁的争用与调度,锁可以被看成是多线程程序访问共享数据时所需要持有的一种排他性资源,因此资源的争用、调度概念对锁也是适用的。
3、锁的粒度,一个锁实例可以保护一个或者多个共享数据。一个锁实例所保护的共享数据的大小被称为该锁的粒度(Granularity)。

显式锁:Lock接口(显示锁的使用)

1、创建Lock接口实例。如果没有特殊需求,我们就可以创建Lock接口的默认实现类ReentrantLock的实例作为显示锁使用。从字面意思上可以看出ReentrantLock是一个可重入锁。
2、在访问共享数据前申请相应的显式锁,我们直接调用Lock.lock()即可。
3、在临界区中访问共享数据。Lock.lock()调用与Lock.unlock()调用之间的代码区域为临界区。对共享数据的访问代码都应该在临界区中。
4、共享数据访问结束后释放锁。虽然释放锁的操作通过调用Lock.unlock()即可实现但是为了避免锁泄露,我们必须将这个调用放在finally,无论临界区代码执行正常结束还是由于其抛出异常而提前退出,相应锁的unlock方法总是可以被执行的,从而避免了锁的泄露。可见,显示锁不像内部锁那样可以由编译器代为规避锁泄露问题。

 package com.lppz.shop.dingding.service.act;

    import sun.awt.SunToolkit;
    
    import javax.xml.crypto.Data;
    import java.util.Date;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    /**
     * @author: 20014915
     * @create: 2019-05-17
     * @description: 描述
     **/
    public class LockBaseCircularSeqGenerator {
        private int sequence = 0;
        private final Lock lock = new ReentrantLock(true);
    
        public int getSequence() {
            lock.lock();
            try {
                if (this.sequence < 999) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    this.sequence++;
                    return sequence;
                } else {
                    setSequence(0);
                    return this.sequence;
                }
            } catch (Exception e) {
               return 0;
            }finally {
                lock.unlock();
            }
        }
    /*    public synchronized int getSequence() {
                if (this.sequence < 999) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    this.sequence++;
                    return sequence;
                } else {
                    setSequence(0);
                    return this.sequence;
                }
        }*/
        public void setSequence(int sequence) {
            this.sequence = sequence;
        }
    
    
        public static void main(String[] args) {
            Date date = new Date();
            final LockBaseCircularSeqGenerator lockBaseCircularSeqGenerator = new LockBaseCircularSeqGenerator();
            for (int i = 0; i < 20; i++) {
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int i1 = 0; i1 < 20; i1++) {
                            System.out.println(lockBaseCircularSeqGenerator.getSequence());
                        }
                    }
                });
                thread.start();
                try {
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
            System.out.println( System.currentTimeMillis() - date.getTime());
        }
    }
显式锁和内部锁的对比

1、内部锁是基于代码块的锁,其基本无灵活性可言。但是简单好用,不容易造成锁泄露。如果内部锁的持有线程一直不释放内部锁的话,共享区的所有线程都将阻塞等待。
2、显示锁比较灵活,功能也比较强大,显示锁有tryLock()方法,可以避免再获取不到锁的时候,不至于一直阻塞等待,但是使用显示锁,容易忘记释放锁,导致锁泄露。

读写锁(read/write lock)

1、读写锁是一种改进型的排他锁,也被称为共享/排他锁。读写锁允许多个线程可以同时读取(只读),共享变量,但是一次只允许一个线程对共享变量进行更新(包括读取之后再更新)。任何线程读取共享变量的时候,其他线程无法更新这些变量;一个线程更新共享变量的时候,其他线程都无法访问该变量。
2、读写锁的功能是通过其扮演的两种角色-----读锁(read lock)和写锁(write lock)实现的,都线程在访问共享变量的时候必须持有相应读写锁的读锁。读锁可以被多个线程同时持有的,因此读锁是共享的(shared),一个线程持有一个读锁的时候并不妨碍其他读线程获取该读锁。写线程在访问共享变量的时候必须持有相应读写锁的写锁。写锁是排他的(Exclusive),即一个线程持有该写锁的时候,其他线程无法获取到相应读写锁的写锁。
3、ReentrantReadWriteLock实现的读写锁是个可重入锁。ReentrantReadWriteLock支持锁的降级,即一个线程持有读写锁的写锁的时候可以获取到该读写锁的读锁。但是不支持锁的升级,即在持有读写锁的读锁的情况下,只有释放了读锁之后才能获取写锁。

锁适用的场景

1、check-then-act操作:一个线程读取共享变量并再次基础上决定器下一个操作是什么。
2、read-modify-write操作:一个线程读取共享数据,并在此基础上更新该数据。
3、多个线程对多个共享变量进行更新:如果这些共享数据之间存在关联关系,那么为了保障操作的原子性操作我们可以考虑使用锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值