java并发编程——一并发基础

为什么使用多线程

  • 多任务的处理,提高处理速度,减少相应时间,更好的体验

  • 随着cpu的核心数量越来越多,提供了充足的硬件基础,使用多线程重复发挥机器的计算能力,合理利用资源

上下文切换

cpu通过给每个线程分配cpu时间片(时间片:一般几十毫秒,是cpu分配给每个线程的时间),实现多线程执行(无论单核与多核)。cpu通过不断切换线程,已达到多个线程执行的效果。

每次切换到另一个线程时,会保存当前线程的任务状态,以便下次切换回来。所以,线程任务从保存到再次切换回来的过程就是一次上下文切换。cpu给线程分配了多少时间片,就代表这个线程使用了多少cpu资源。

  • 多线程一定快?
public class ConcurrencyTest {
    private static final long count = 1000000l;

    public static void main(String[] args) throws InterruptedException {
        concurrency();
        serial();

    }

    private static void concurrency() throws InterruptedException {

        long start = System.currentTimeMillis();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {

                int a = 0;
                for (long i = 0; i < count; i++) {
                    a += 5;
                }

            }
        });
        thread.start();
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        // 
        // long time = System.currentTimeMillis() - start;
        // thread.join();

        thread.join();
        long time = System.currentTimeMillis() - start;
        System.out.println("concurrency:" + time + "ms,b=" + b);
    }

    public static void serial() {

        long start = System.currentTimeMillis();
        int a = 0;
        for (long i = 0; i < count; i++) {
            a += 5;
        }
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }

        long time = System.currentTimeMillis() - start;
        System.out.println("serial:" + time + "ms,b=" + b + ",a=" + a);
    }
}
count=10000---------concurrency:1ms .  b=-10000
count=10000---------serial:0ms .  b=-10000,a=50000

一个死锁示例:

package com.concurrent.partOne;

public class DeadLockDemon {
    private static String A = "A";
    private static String B = "B";

    public static void main(String[] args) {
        new DeadLockDemon().deadLock();
    }

    private void deadLock() {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (A) {
                    try {
                        Thread.sleep(2000);

                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    synchronized (B) {
                        System.out.println(1);
                    }

                }

            }

        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (B) {
                    synchronized (A) {

                        System.out.println(2);
                    }
                }

            }
        });

        t1.start();
        t2.start();
    }
}

运行javaVisualVM:

"DestroyJavaVM" prio=6 tid=0x00000000004db800 nid=0xa494 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
    - None

"Thread-1" prio=6 tid=0x0000000006528000 nid=0xa980 waiting for monitor entry [0x0000000006ebf000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at com.concurrent.partOne.DeadLockDemon$2.run(DeadLockDemon.java:40)
    - waiting to lock <0x000000009beee950> (a java.lang.String)
    - locked <0x000000009beee988> (a java.lang.String)
    at java.lang.Thread.run(Thread.java:662)

   Locked ownable synchronizers:
    - None

"Thread-0" prio=6 tid=0x0000000006527800 nid=0xa9fc waiting for monitor entry [0x0000000006dbf000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at com.concurrent.partOne.DeadLockDemon$1.run(DeadLockDemon.java:25)
    - waiting to lock <0x000000009beee988> (a java.lang.String)
    - locked <0x000000009beee950> (a java.lang.String)
    at java.lang.Thread.run(Thread.java:662)

   Locked ownable synchronizers:
    - None

"Low Memory Detector" daemon prio=6 tid=0x0000000006523000 nid=0xa9c0 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
    - None

并发的缺陷

  • 等待共享资源时,性能降低

  • 需要处理线程的额外CPU消耗(上下文切换等)

  • 糟糕的并发设计导致不必要的复杂度

  • 可能会产生 死锁、饿死、竞争、活锁(各自任务可运行,整体任务无法完成)问题

    饿死(当饥饿到一定程度的进程所赋予的任务即使完成也不再具有实际意义时称该进程被饿死)
    考虑一台打印机分配的例子,当有多个进程需要打印文件时,系统按照短文件优先的策略排序,该策略具有平均等待时间短的优点,似乎非常合理,但当短文件打印任务源源不断时,长文件的打印任务将被无限期地推迟,导致饥饿以至饿死。

如果要大量编写复杂多线程代码,可以尝试使用Erlang

接下来,我们从基础开始,深入原理,系统的学习java 并发

什么是线程:

操作系统运算调度的最小单元,轻量级的进程,一个线程包含独立的计数器、栈、局部变量等属性,并且线程间可以共享堆内存。所谓的多线程程序,也就是处理器通过上下文切换,给不同线程分配时间片,让人感觉线程是同时执行的。

为什么要多线程(并发编程):

  • 充分利用多核处理器
    一个进程下会有多个线程,一个线程的运行会占用一个处理器核心。现在多核cpu已经司空见惯,如果我们编程还是单线程,那么多核cpu中只会有一个核心被使用,其他核心被闲置。为了重复利用cpu多核资源,提高运算能力,我们使用多线程,同一时间可以在cpu的多核上运行多个线程。

  • 更快的响应体验

    比如我们请求一个页面,如果是单线程,会挨个获取这个页面上的视频、图片、文字等等资源;如果是多线程,会并发回去这个页面上的资源,以缩短页面加载的时间。更快的速度,更好的用户体验。

线程的6种状态

  • NEW: 线程构建完毕,但是还没有开始运行(还未执行thread.start());

  • RUNNABLE: 线程的就绪与线程的运行都称为RUNNABLE状态;

  • BLOCKED: 阻塞状态。线程等待获取其他线程通过Synchronized持有的锁时,呈现BLOCKED状态

  • WAIT: 等待状态;Lock锁阻塞;object.wait();Thread.join();LockSupport.part()呈现的状态

  • TIME_WAITING: 有超时限制的方法通常执行后呈现TIME_WAITING状态.相当于在WAIT状态基础上增加了超市限制。如,Thread.Sleep(long n);;Thread.join(long n)

  • TERMINATE: 线程结束的状态

Demon线程

后台的守护线程,通过thread.setDemon(true)设置。
当进程中没有非Demon线程执行时,该进程立刻结束,也就是说Demon线程也会立刻结束。
注意:无法保证Demon线程中的finally块一定执行。

线程的构建

线程的构建也就是初始过程,通常一个线程由其父进程创建:父进程为子进程分配空间,子进程继承InheritableThreadLocal、继承了classLoader、继承是否Demon,并且子进程获取到了一个唯一ID

Thread.init(…)

 private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name.toCharArray();

        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

线程的中断

线程的中断可以理解为线程中一个标识符的变更,一个线程向改变了另一个线程的“中断标识符”。
Thread.isInterrupted(): true:该线程已经被中断;false没有被中断
Thread.interrupted(): true:该线程已经被中断;false没有被中断;该方法的执行总是会把中断标识置为true;

一些方法可以响应中断抛出InterruptedException,需要注意异常抛出后Thread.isInterrupted为false;

volatile

….. volatile Type variable;
特性:可见性、原子性
会在编译后的字节码中多出来一个lock指令,该指令会使线程对这个变量的读取都从内从中读,对这个变量的写都会立刻刷入主存中。由此保证了变量的可见性
volaitle变量的读-写均具有原子性(处理器层面保证的原子性,注意对比java如何保证原子性),类似与使用了一个锁对普通变量的读、写操作做同步。

volatile原理:在汇编语言上可以看到,volatile变量会比普通变量多出一个“Lock”指令,通过Lock指令实现volatile。
对于一个多核处理器来说,lock指令做两件事情:
1.使当前处理器中的缓存数据写入到主内存中,并且lock指令通过锁总线或者锁缓存保证对数据读写的原子性(处理器保证的原子性);
2.缓存数据写回主存的操作会导致其他处理器的缓存失效。(通过总线通信保证缓存一致性)

volatile内存语义:
volatile变量的读取:当读取一个volatile变量时,JMM会把本地内存中的变量置为无效,从主从中直接读取;
volatile变量的写入:当volatile写入时,会直接把变量值刷入主从中。
另外,volatile的
如何实现volatile内存语义
主要通过编译器、处理器重排序做限制实现的。
对于编译器编译时必须遵守编译器重排序表,volatile写之前的操作禁止重排序放到写之后,volatile读之后的操作禁止重排序放到读之前;volatile写 之后的volatile读禁止重排序;
对于cpu来说,在字节码上通过插入内存屏障禁止重排序。

谨慎使用volatile
volatile正确使用的原则是:
1.volaitle变量的写操作不依赖于当前volaitle变量(volatile i++并不能保证线程安全);
2.volatile没有被放在包含其他变量的不变式中

@NotThreadSafe 
public class NumberRange {
    private int lower, upper;

    public int getLower() { return lower; }
    public int getUpper() { return upper; }

    public void setLower(int value) { 
        if (value > upper) 
            throw new IllegalArgumentException(...);
        lower = value;
    }

    public void setUpper(int value) { 
        if (value < lower) 
            throw new IllegalArgumentException(...);
        upper = value;
    }
}

可见性

当一个线程对一个变量做了写操作,随后的另一个线程能否看到(读到)之前那个线程的写操后的值呢?这个问题就是可见性问题!答案是未正确同步的情况下,不一定能看到!

为什么会有可见性问题?写了为什么会看不见呢??

在对数据写入时,计算机为了提升性能,并不会每次都把这个更新后的数据写入到(主存)堆内存中,而是暂时存到了本地缓存中,稍后刷入主存。由于没有及时写到主存中,其他线程无法看到这个变量的更新,依旧会使用更新之前的值做计算。

这个变量的更新是否可以及时被其他线程看见,我们成为变量的可见性。

synchronized

synchronized保证线程之间的互斥性、可见性、原子性。

synchronized块通过使用 monitorenter\monitorexit指令实现对代码的互斥访问;
synchronized方法通过依靠方法修饰符上的ACC_SYNCHRONIZED指令实现互斥。
本质上都是通过获取一个对象的monitor,这个获取是排他的。也就是同一时刻只有一个线程可以获取被synchronized保护对象的monitor.

更详细的底层实现请查看:
(synchronized\Lock\volatile) 锁机制原理及关联

©️2020 CSDN 皮肤主题: 点我我会动 设计师:上身试试 返回首页