Java多线程之深入理解与并发编程

在前面几篇博客介绍了多线程创建方式、线程安全、线程通信等内容,本篇博客继续介绍一下Java并发编程内容。

1、线程三大特性

①、原子性

所谓原子性就是不可再分割。比如假设某个操作包括5个步骤,这5个步骤要么全部执行完,要不都不执行,不能说执行了其中几个步骤就不管了。
原子性其实就是保证数据一致、线程安全。比如i = i + 1,在多线程同时进行此操作时,会发生线程不安全,因为该语句包括取出i的值,然后再自增。可能当你刚取出后,其它线程将它修改。
(这与数据库中事务的原子性的概念是相通的。)

②、可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
比如i = i + 1语句,当某一时刻i=50,此时线程A取出i=50,还没来得及加1时,线程B将i修改为了51,这时线程A应该需要立即更正i=51才能得到正确的结果。
然而这种操作基本不可能实现,我们退而求其次,将i变量的修改设置锁,每次只能由一个进程进行(A或B,不能A、B同时修改),修改成功后,再去更新其它线程中的i的值。

③、有序性

程序执行的顺序按照代码的先后顺序执行。一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

 int a = 10;    //语句1
 int r = 2;    //语句2
 a = a + 3;    //语句3
 r = a*a;     //语句4

因为重排序,它还可能执行顺序为 2-1-3-4、1-3-2-4等,但绝不可能 2-1-4-3,因为这打破了依赖关系。
显然重排序对单线程运行是不会有任何问题,而多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。

2、内存模型

共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,对其它线程可见。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。

本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
在这里插入图片描述

3、volatile关键字

先不说volatile关键字有什么作用,先看一个示例:

在这里插入图片描述

package cn.hestyle.demo;

class VolatileTestThread extends Thread {
    public boolean flag = true;
    @Override
    public void run() {
        System.out.println("开始执行子线程....");
        //当flag=true时,死循环运行
        while (flag) {
        }
        System.out.println("线程停止");
    }
    public void setRunState(boolean flag) {
        this.flag = flag;
    }
}

/**
 * description: demo_01
 *
 * @author hestyle
 * @version 1.0
 * @className multi_thread_project_01->MultiThreadDemo01
 * @date 2020-02-09 15:35
 **/
public class MultiThreadDemo01 {
    public static void main(String[] args) throws InterruptedException {
        //main方法所在线程称为主线程,在main中创建的线程称为子线程
        VolatileTestThread volatileTestThread = new VolatileTestThread();
        volatileTestThread.start();
        Thread.sleep(3000);
        //在主线程中修改了volatileTestThread的变量flag
        volatileTestThread.setRunState(false);
        System.out.println("flag 已经设置成false");
        Thread.sleep(1000);
        //再次打印volatileTestThread.flag,看是否修改成功
        System.out.println(volatileTestThread.flag);
    }
}

根据上面Java共享内存模型可知,主线程main复制了子线程volatileTestThread.flag到自己的本地内存,主线程修改volatileTestThread.flag的时候只修改了自己的本地内存变量,没有告诉子线程volatileTestThread的本地内存,这样就导致了main修改了flag=false,volatileTestThread线程也没有停止。

我们只要在volatileTestThread.flag变量增加volatile关键字修饰。
在这里插入图片描述
也就是说volatile关键字的作用就是当某个线程修改了它自己的本地内存中的值,其它的线程中的值都会被立即更新。即线程可见性特性。

v o l a t i l e 线 \color{red}但是volatile关键字不能保证线程安全性

package cn.hestyle.demo;

class VolatileTestThread extends Thread {
    private volatile static int count = 0;
    @Override
    public void run() {
        for (int i = 0; i < 10000; ++i) {
            count++;
        }
        System.out.println("count = " + count);
    }
}

/**
 * description: demo_01
 *
 * @author hestyle
 * @version 1.0
 * @className multi_thread_project_01->MultiThreadDemo01
 * @date 2020-02-09 15:35
 **/
public class MultiThreadDemo01 {
    public static void main(String[] args) throws InterruptedException {
        VolatileTestThread volatileTestThread1 = new VolatileTestThread();
        VolatileTestThread volatileTestThread2 = new VolatileTestThread();
        VolatileTestThread volatileTestThread3 = new VolatileTestThread();
        VolatileTestThread volatileTestThread4 = new VolatileTestThread();
        volatileTestThread1.start();
        volatileTestThread2.start();
        volatileTestThread3.start();
        volatileTestThread4.start();
    }
}

在这里插入图片描述
也就是说Volatile只保证了立即更新其它线程的本地内存中的副本,但是无法解决两个线程同一时刻对某个变量的修改出现的线程不安全问题。

在之前的博客就介绍过,java解决线程安全问题用的线程同步(锁)来实现。
在这里插入图片描述

AtomicInteger原子类也可以实现

在这里插入图片描述
除了AtomicInteger原子类,还有其它原子类型
在这里插入图片描述

volatilesynchronized区别
volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法
②volatile只能保证数据的可见性,不能用来同步,
	因为多个线程并发访volatile修饰的变量不会阻塞。
	synchronized不仅保证可见性,而且还保证原子性,因为,
	只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。
	多个线程争抢synchronized锁对象时,会出现阻塞。

总的来说volatile保证可见性,synchronized保证可见性、原子性,而线程安全包括可见性、原子性,因此volatile不能保证线程不安全,synchronized能保证线程安全。

4、ThreadLocal

这个类就是提供一种操作——可将对象放入本地线程中,只有线程自己可访问
在这里插入图片描述

void set(Object value) 设置当前线程的线程局部变量的值。
 •	public Object get() 该方法返回当前线程所对应的线程局部变量。
 •	public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。
 	需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,
 	所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
 •	protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。
 	这个方法是一个延迟调用方法,在线程第1次调用get()set(Object)时才执行,
 	并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

示例:两个变量各自生成自己的编号

package cn.hestyle.demo;


class ThreadLocalTestThread implements Runnable {
    /**
     * 本地线程,put(当前线程,变量)
     */
    private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    @Override
    public void run() {
        //初始化threadLocal存储的变量
        threadLocal.set(0);
        for (int i = 0; i < 5; ++i) {
            Integer integer = threadLocal.get() + 1;
            //更新threadLocal存储的变量
            threadLocal.set(integer);
            System.out.println(Thread.currentThread().getName() + "生成编号:" + integer);
        }

    }
}

/**
 * description: demo_01
 *
 * @author hestyle
 * @version 1.0
 * @className multi_thread_project_01->MultiThreadDemo01
 * @date 2020-02-09 15:35
 **/
public class MultiThreadDemo01 {
    public static void main(String[] args) throws InterruptedException {
        ThreadLocalTestThread threadLocalTestThread = new ThreadLocalTestThread();
        //创建两个线程
        Thread thread_1 = new Thread(threadLocalTestThread, "线程一");
        Thread thread_2 = new Thread(threadLocalTestThread, "线程二");
        thread_1.start();
        thread_2.start();
    }
}

在这里插入图片描述
我之前搞web项目时,我把数据库的connection放入到ThreadLocal中。
那时还没使用持久层框架,在Dao层通过connection执行sql语句,而Service层需要进行事务控制,需要用到Dao层获取的那个connection(只有同一个connection才能进行事务提交)。
但每次都把connection传回Service层的方法比较麻烦。所以Dao层第一次获取的connection放入ThreadLocal对象中保存,当下次这个线程(不管是在DaoService层)需要connection时,直接从ThreadLocal对象中获取即可。

5、线程池

由于线程的开启非常耗资源,因此频繁的开启、销毁线程会降低程序的执行效率。
此时我们就可以抽象出一个对象,让它来管理线程开启、销毁。也就是说我们要线程的时候就向它要,用完了就还给它。究竟它什么时候开启、销毁某个线程,这与我们无关。我们称它为线程池(通俗的理解,里面放了线程,要用就拿,用完了还给它)。

创建线程池的四种方式
①、newCachedThreadPool

创建一个可缓存不定长线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

public class MultiThreadDemo01 {
    public static void main(String[] args) throws InterruptedException {
        //创建一个可缓存不定长线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            cachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "---" + index);
                }
            });
        }
        //关闭线程池
        cachedThreadPool.shutdown();
    }
}

在这里插入图片描述

②、newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

public class MultiThreadDemo01 {
    public static void main(String[] args) throws InterruptedException {
        //创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int index = i;
            fixedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "---" + index);
                }
            });
        }
        //关闭线程池
        fixedThreadPool.shutdown();
    }
}

在这里插入图片描述

③、newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。

public class MultiThreadDemo01 {
    public static void main(String[] args) throws InterruptedException {
        //创建一个定长线程池,支持定时及周期性任务执行。
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int index = i;
            scheduledThreadPool.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "延迟1秒...");
                }
            }, 3, TimeUnit.SECONDS);
        }
        //关闭线程池
        scheduledThreadPool.shutdown();
    }
}

在这里插入图片描述

④、newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

public class MultiThreadDemo01 {
    public static void main(String[] args) throws InterruptedException {
        //创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
        ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int index = i;
            singleThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "-------"+ index);
                }
            });
        }
        //关闭线程池
        singleThreadPool.shutdown();
    }
}

在这里插入图片描述

线程池的作用:
1. 线程池改进了一个应用程序的响应时间。由于线程池中的线程已经准备好且等待被分配任务,应用程序可以直接拿来使用而不用新建一个线程。
2. 线程池节省了CLR 为每个短生存周期任务创建一个完整的线程的开销并可以在任务完成后回收资源。
3. 线程池根据当前在系统中运行的进程来优化线程时间片。
4. 线程池允许我们开启多个任务而不用为每个线程设置属性。
5. 线程池允许我们为正在执行的任务的程序参数传递一个包含状态信息的对象引用。
6. 线程池可以用来解决处理一个特定请求最大线程数量限制问题。

以上就是本篇博客主要内容,有点杂,比较重要的点是线程的三大特性、ThreadLocal本地线程、volatile关键字、AtomicInteger等原子类。

发布了976 篇原创文章 · 获赞 230 · 访问量 20万+

猜你喜欢

转载自blog.csdn.net/qq_41855420/article/details/104244216