程序员最近都爱上了这个网站  程序员们快来瞅瞅吧!  it98k网:it98k.com

本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长

+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

多线程核心技术(1)-线程的基本方法

发布于2019-08-30 11:04     阅读(726)     评论(0)     点赞(7)     收藏(1)


多线程核心技术(1)-线程的基本方法

进程和线程

  了解多线程首先要了解进程和线程的概念,在操作系统里,进程是资源分配最小单位,一般情况下一个应用就会在计算机系统内开启一个进程,线程可以理解为进程中多个独立运行的子任务,是操作系统能够进行调度运算的最小单位,但是线程不拥有资源,只能共享进程中的数据,所以多个线程对进程中某个数据同时进行修改时,就会产生线程安全问题。由于一个进程中允许存在多个线程,所以在多线程中,如何处理线程并发和线程之间通信的问题,是学习多线程编程的重点。
复制代码

多线程的使用

​ 在java中,创建一个线程一般有两种方式,继承Thread类或者实现Runable接口,重写run方法即可,然后调用start()方法即可以开启一个线程并执行。如果想要获取当前线程执行返回值,在jdk1.5以后,可以通过实现Callable接口,然后借助FutureTask或者线程池得到返回值。由于线程的执行具有随机性,所以线程的开启顺序并不意味线程的执行顺序。

1、继承Thread创建一个线程
/**
 * 继承Thread 重写Run方法
 * 类是单继承的,生产环境中如果此类无需实现其他接口 可使用这种方法创建线程
 * User: lijinpeng
 * Created by Shanghai on 2019/4/13.
 */
@Slf4j
public class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }
    @Override
    public void run() {
        log.info("Hi,I am a thread extends Thread,My name is:{}", this.getName());
    }
}

复制代码
2、实现Runable接口创建一个线程

/**
 * 实现Runnable接口
 * 类允许有多个接口实现 生产中一般使用这种方式创建线程
 * 线程的开启还需要借助于Thread实现
 * User: lijinpeng
 * Created by Shanghai on 2019/4/13.
 */
@Slf4j
@Getter
public class ThreadRunable implements Runnable {

    private String name;

    public ThreadRunable(String name) {
        this.name = name;
    }

    public void run() {
        log.info("Hi,I am a thread implements Runnable,My name is:{}", this.getName());
    }
}
复制代码
3、实现Callable 接口 获取线程执行结果
/**
 * 实现Callable接口创建获取具有返回值的线程
 * 线程使用需要借助FutureTask和Thread,或者使用线程池
 * User: lijinpeng
 * Created by Shanghai on 2019/4/13.
 */
@Slf4j
public class CallableThread implements Callable<Integer> {

    private AtomicInteger seed;
    @Getter
    private String name;

    public CallableThread(String name, AtomicInteger seed) {
        this.name = name;
        this.seed = seed;
    }

    public Integer call() throws Exception {
        //使用并发安全的原子类生成一个整数
        Integer value = seed.getAndIncrement();
        log.info("I am thread implements Callable,my name is:{} my value is:{}", this.name, value);
        return value;
    }
}
复制代码
4、验证三种线程的启动

/**
 * User: lijinpeng
 * Created by Shanghai on 2019/4/13.
 */
@Slf4j
public class ThreadTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
     threadTest();
     runableTest();
     callableTest();
    }

    public static void threadTest() {
        MyThread threadA = new MyThread("threadA");
        threadA.start();
    }

    public static void runableTest() {
        ThreadRunable runable = new ThreadRunable("threadB");
        //需要借助Thread来开启一个新的线程
        Thread threadB = new Thread(runable);
        threadB.start();
    }

    public static void callableTest() throws ExecutionException, InterruptedException {
        AtomicInteger atomic = new AtomicInteger();
        CallableThread threadC1 = new CallableThread("threadC1", atomic);
        CallableThread threadC2 = new CallableThread("threadC2", atomic);
        CallableThread threadC3 = new CallableThread("threadC3", atomic);
        FutureTask<Integer> task1 = new FutureTask<Integer>(threadC1);
        FutureTask<Integer> task2 = new FutureTask<Integer>(threadC2);
        FutureTask<Integer> task3 = new FutureTask<Integer>(threadC3);
        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);
        Thread thread3 = new Thread(task3);
        thread1.start();
        thread2.start();
        thread3.start();
        while (task1.isDone()&&task2.isDone()&&task3.isDone())
        {
        }
        log.info(threadC1.getName()+"执行结果:"+String.valueOf(task1.get()));
        log.info(threadC2.getName()+"执行结果:"+String.valueOf(task2.get()));
        log.info(threadC2.getName()+"执行结果:"+String.valueOf(task3.get()));
    }
}
复制代码

以下是程序执行结果:

结论:

  1. 这三种方式都可以开启一个线程,实现具体开启线程的任务还是交给Thread类实现,因此对于Runable和Callable来说,最后都是要借助于Thread开启线程,类是单继承的,接口是多实现的,由于生产环境业务复杂性,一个类可能会有其他功能,因此一般使用接口实现的方式。
  2. 从上面的线程声明顺序和执行顺序结果来看,线程的执行是无序的,CPU执行任务是采用轮询机制来提高CPU使用率,在线程获取执行资源进行就绪队列后 才会再次被CPU调用,而这个过程跟程序无关。

线程的生命周期

​ 一个线程的运行通常伴随着线程的启动、阻塞、停止等过程,线程启动可以通过Thread类的start()方法执行,由于多线程可能会共享进程数据,阻塞一般发生在等待其他线程释放进程某块资源的过程,当线程执行完毕,可以自动停止,也可以通过调用stop()强制终止线程,或者在线程执行过程中由于异常导致线程终止,了解线程的生命周期是学习多线程最重要的理论基础。

​ 下图为线程的生命周期以及状态转换过程

新建状态

当通过Thread thead=new Thread()创建一个线程的时候,该线程就处于 new 状态,也叫新建状态。

就绪状态

当调用thread.start()时,线程就进入了就绪状态,在该状态下线程并不会运行,只是表示线程进入可供CPU调用的就绪队列,具备运行条件。

运行状态

当线程获得了JVM中线程调度器的调度时候,线程就进入运行状态,会执行重写的 run方法。

阻塞状态

此时的线程仍处于活动状态,但是由于某种原因失去了CPU对其调度权利,具体原因可分为以下几种

  1. 同步阻塞

    此时由于线程A需要获取进程的资源1,但是资源1被线程B所持有,必须等待线程B释放资源1之后,该线程才会进入资源1的就绪线程池里,获取到资源1后,等待被CPU调度器调度再次运行。同步阻塞一般出现在线程等待某项资源的使用权利,在程序中使用锁机制会产生同步阻塞。
    复制代码
  2. 等待阻塞

    当执行Thread类的wait() 和join()方法时,会造成当前线程的同步阻塞,wait()会使当前线程暂停运行,并且释放所拥有的锁,可以通该线程要等待的某个类(Object)的notify()或者notifyall()方法唤醒当前线程。join()方法会阻塞当前线程,直到线程执行完毕,可以通过join(time)指定等待的时间,然后唤醒线程。
    复制代码
  3. 其他阻塞

    调用sleep()方法主动放弃所占用的CPU资源,这种方式不会释放该线程所拥有的锁,或者调用一个阻塞式IO方法、发出了I/O请求,进入这种阻塞状态。被阻塞的线程会在合适的时候(阻塞解除后)重新进入就绪状态,重新等待线程调度器再次调度它。
    复制代码
死亡状态

​ 当线程执行完run方法时,就会自动终止或者处于死亡状态,这是线程的正常死亡过程。或者通过显示调用stop()终止线程,但不安全。还可以通过抛异常法终止线程。

实例变量与线程安全

多线程访问进程资源

​ 在多线程任务应用中如果多个线程执行之间使用了进程的不同资源,即运行中不共享任何进程资源,各线程运行不受影响,且不会产生数据安全问题。如果多个线程共享了进程的某块资源,会同时修改该块资源数据,产生最终结果与预期结果不一致的情况,导致线程安全问题。如图:

主内存与工作内存

​ Java内存模型分为主内存,和工作内存。主内存是所有的线程所共享的,工作内存是每个线程自己有一个,不是共享的。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值),都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者之间的交互关系如下图:

线程对主存的操作指令:lock,unlock,read,load,use,assign,store,write操作

  • read-load阶段从主内存复制变量到当前工作内存

  • use和assign阶段执行代码改变共享变量值

  • store和write阶段用工作内存数据刷新主存对应变量的值。

  • store and write执行时机

    1、java内存模型规定不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,没必要    连续执行,也就是说read与load之间、store与write之间是可插入其他指令的。
    2、不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。变量在当前线程中改变一次其实就是一次assign,而且不允许丢弃最近的assign,所以必定有一次store and write,又根据第一条read and load 和store and write 不能单一出现,所以有一次store and write 必定有一次 read and load,因此推断出,变量在当前线程中每一次变化都会执行 read 、 load 、use 、assign、store、write
    3、volatile修饰变量,是在use和assign阶段保证获取到的变量永远是跟主内存变量保持同步
    复制代码
    非线程安全问题

    ​ 在多线程环境下use和assign是多次出现的,但此操作并不是原子性的,也就是说在线程A执行了read和load从主内存 加载过变量C后,此时如果B线程修改了主内存中变量C的值,由于线程A已经加载过变量C,无法感知数据已经发生变化,即从线程A的角度来看,工作内存和主内存的变量A已经不再同步,当线程A使用use和assign时,就会出现非线程安全的问题。解决此问题可以通过使用volatile关键字修饰,volatile可以保证线程每次使用use和assign时,都从主内存中拿到最新的数据,而且可以防止指令重排,但volatile仅仅是保证变量的可见性,无法使数据加载的几个步骤是原子操作,所以volatile并不能保证线程安全。

    如下代码所示:

    多个业务线程访问用户余额balance,最终导致扣款总金额超过了用户余额,由线程不安全导致的资损情景.而且每个业务线程都扣款了两次,也说明了线程启动时需要将balance加载到工作内存中,之后该线程基于加载到的balance操作,其他线程如何改变balance值,对当前业务线程来说都是不可见的。

    /**
     * 业务订单代扣线程 持续扣费
     * User: lijinpeng
     * Created by Shanghai on 2019/4/13.
     */
    @Slf4j
    public class WithHoldPayThread extends Thread {
        //缴费金额
        private Integer amt;
        //业务类型
        private String busiType;
    
        public WithHoldPayThread(Integer amt, String busiType) {
            this.amt = amt;
            this.busiType = busiType;
        }
    
        @Override
        public void run() {
            int payTime = 0;
            while (WithHodeTest.balance > 0) {
                synchronized (WithHodeTest.balance) {
                    boolean result = false;
                    if (WithHodeTest.balance >= amt) {
                        WithHodeTest.balance -= amt;
                        result = true;
                        payTime++;
                    }
                    log.info("业务:{} 扣款金额:{} 扣款状态:{}", busiType, amt,result);
                }
            }
            log.info("业务:{} 共缴费:{} 次", busiType, payTime);
        }
    }
    复制代码

    测试函数

    /**
     * User: lijinpeng
     * Created by Shanghai on 2019/4/13.
     */
    public class WithHodeTest {
        //用户余额 单位 分
        public static volatile Integer balance=100;
    
        public static void main(String[] args) {
            WithHoldPayThread phoneFare = new WithHoldPayThread(50, "缴存话费");
            WithHoldPayThread waterFare = new WithHoldPayThread(50, "缴存水费");
            WithHoldPayThread electricFare = new WithHoldPayThread(50, "缴存电费");
            phoneFare.start();
            waterFare.start();
            electricFare.start();
        }
    }
    复制代码

    执行结果:

实验结果证明,每个线程的扣款都成功了,这就导致了线程安全问题,解决这个问题最简单的做法是在run方法里面加synchronized修饰,并且对balance使用volatile修饰就可以了。

   //用户余额 单位 分
    public static  volatile Integer balance=100;
复制代码
 @Override
    public void run() {
        int payTime = 0;
        while (WithHodeTest.balance > 0) {
            synchronized (WithHodeTest.balance) {
                boolean result = false;
                if (WithHodeTest.balance >= amt) {
                    WithHodeTest.balance -= amt;
                    result = true;
                    payTime++;
                }
                log.info("业务:{} 扣款金额:{} 扣款状态:{}", busiType, amt,result);
            }
        }
        log.info("业务:{} 共缴费:{} 次", busiType, payTime);
    }
复制代码

执行结果:

线程的基本API

线程的停止



所属网站分类: 站长公众号

作者:听爸爸的话

链接:https://www.pythonheidong.com/blog/article/71006/8ab4ea8e8c678b0d9a07/

来源:python黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

7 0
收藏该文
已收藏

评论内容:(最多支持255个字符)