多线程与高并发

韩愈说过这样一句话:“业精于勤荒于嬉,行成于思毁于随””。天才就是无止境刻苦勤奋的努力。成绩优与良;才思浓与淡,都是由勤奋注定的。


概念

进程

进程指正在运行的程序,进程拥有一个完整的、私有的基本运行资源集合。通常,每个进程都有自己的内存空间。

进程往往被看作是程序或应用的代名词,然而,用户看到的一个单独的应用程序实际上可能是一组相互 协作的进程集合。

为了便于进程之间的通信,大多数操作系统都支持进程间通信(IPC),如pipes 和sockets。IPC不仅支持同一系统上的通信,也支持不同的系统。IPC通信方式包括管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等方式,其中 Socket和Streams支持不同主机上的两个进程IPC。

线程

线程有时也被称为轻量级的进程。进程和线程都提供了一个执行环境,但创建一个新的线程比创建一个 新的进程需要的资源要少。

线程是在进程中存在的 — 每个进程最少有一个线程。线程共享进程的资源,包括内存和打开的文件。这样提高了效率,但潜在的问题就是线程间的通信。

多线程的执行是Java平台的一个基本特征。每个应用都至少有一个线程 – 或几个,如果算上“系统”线程的话,比如内存管理和信号处理等。但是从程序员的角度来看,启动的只有一个线程,叫主线程。

简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程。

并发和并行

  • 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
  • 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
  • 在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群

线程安全

基本概念

  1. 何谓竞态条件:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。在临界区中使用适当的同步就可以避免竞态条件,如使用synchronized或者加锁机制。
  2. 何谓线程安全:允许被多个线程同时执行的代码称作线程安全的代码。线程安全的代码不包含竞态条件。

对象的安全

局部基本类型变量:局部变量存储在线程自己的栈中。也就是说,局部变量永远也不会被多个线程共享。所以,基础类型的 局部变量是线程安全的。下面是基础类型的局部变量的一个例子:


public class ThreadTest {
public static voidmain(String[]args){ MyThread share = new MyThread();
	for (int i=0;i<10;i++){
		new Thread(share,"线程"+i).start(); }
	}
}


class MyThread implementsRunnable{
	public void run() 
  { int a =0;
		++a;
		System.out.println(Thread.currentThread().getName()+":"+a);
   }
}



//打印结果
线程0:1
线程1:1
线程2:1
线程3:1
线程4:1
线程5:1
线程6:1
线程7:1
线程8:1
线程9:1


无论多少个线程对run()方法中的基本类型a执行++a操作,只是更新当前线程栈的值,不会影响其他线程,也就是不共享数据;

对象的局部引用和基础类型的局部变量不太一样,尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内。所有的对象都存在共享堆中。如果在某个方法中创建的对象不会逃逸出(即该对象不会被其它方法获得,也不会被非局部变量引用 到)该方法,那么它就是线程安全的。实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。

对象成员存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。如果两个线程同时调用同一个实例上的同一个方法并且有更新操作,就会有竞态条件问题。

JAVA内存模型

线程之间的通信

线程的通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种共享内 存和消息传递。Java的并发采用的是共享内存模型。

Java 内存模型结构

Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度 来 看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory) 中, 每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。 本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他 的硬件和编译器优化。

从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

  1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

CAS乐观锁

乐观锁:不加锁,假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。其实现方式有一种比较典型的就是Compare and Swap( CAS )。

从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

CAS的缺点:

1.CPU开销较大 在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

2.不能保证代码块的原子性 CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

Synchronized块

概念

Java 中的同步块用 synchronized 标记。 同步块在 Java 中是同步在某个对象上。 所有同步在一个对象上 的同步块在同时只能被一个线程进入并执行操作。所有其他等待进入该同步块的线程将被阻塞,直到执行该同步块中的线程退出。

Synchronized块的几种方式
  • 实例方法
  • 静态方法
  • 实例方法中的同步块
  • 静态方法中的同步块

上述同步块都同步在不同对象上。实际需要那种同步块视具体情况而定。

下面是一个同步的实例方法:



//不同实例调用不会阻塞
public class MethodSync {
    public synchronized void test(){
        try {
            System.out.println(Thread.currentThread().getName() + " test 进入了同步方法");
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName() + " test 休眠结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

/*
* 每个线程都会重新创建一个新的对象所以不会阻塞
*/
public class MyThread extends Thread {
    @Override
    public void run() {
        MethodSync sync = new MethodSync();
        System.out.println(Thread.currentThread().getName() + " test 准备进入");
        sync.test();
    }
}

public class Test {
    public static void main(String[] args) {
        new MyThread().start();
        new MyThread().start();
    }
}

//Thread-1 test 准备进入
//Thread-0 test 准备进入
//Thread-1 test 进入了同步方法
//Thread-0 test 进入了同步方法
//Thread-0 test 休眠结束
//Thread-1 test 休眠结束
//同一个实例调用会阻塞
public class MethodSync {
    public synchronized void test1(){
        try {
            System.out.println(Thread.currentThread().getName() + " test1 进入了同步方法");
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName() + " test1 休眠结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
/**
* 每个线程用同一个MethodSync对象调用test1()所以线程阻塞
*/
public class MyThread extends Thread {
    static MethodSync sync = new MethodSync();
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " test 准备进入");
        sync.test1();
    }
}

public class Test {
    public static void main(String[] args) {
        new MyThread().start();
        new MyThread().start();
    }
}


//Thread-0 test1 准备进入
//Thread-0 test1 进入了同步方法
//Thread-1 test1 准备进入
//Thread-0 test1 休眠结束
//Thread-1 test1 进入了同步方法
//Thread-1 test1 休眠结束

synchronized关键字锁住了调用当前方法的当前实例,如果不同实例不受同步锁synchronized关键字影响,如果相同实例调用的当前方法则受关键字synchronized约束。

同步代码块传参变量对象 (锁住的是变量对象)
  • 同一个属性对象才会实现同步
// 敲重点   同一个属性对象才会实现同步
//  Integer 负128-正127区间的数是放在缓存里的内存地址一致是同一个属性对象
//  如果不在这里区间的则会创建不同的对象不受同步锁控制
public class MethodSync {
    public Integer lockObject;
    public MethodSync(Integer lockObject) {
        this.lockObject = lockObject;
    }
    
    //锁住了实例中的成员变量
    public void test2() {
        synchronized (lockObject) {
            try {
                System.out.println(Thread.currentThread().getName() + " test2 进入了同步方法");
                Thread.sleep(5000);
                System.out.println(Thread.currentThread().getName() + " test2 休眠结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
	
}


public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " test 准备进入");
        MethodSync sync = new MethodSync(127);
        sync.test2();
    }
}


public class Test {
    public static void main(String[] args) {
        new MyThread().start();
        new MyThread().start();
    }
}

//Thread-0 test2 准备进入
//Thread-1 test2 准备进入
//Thread-0 test2 进入了同步方法
//Thread-0 test2 休眠结束
//Thread-1 test2 进入了同步方法
//Thread-1 test2 休眠结束

同一个实例对象的成员属性肯定是同一个,此处列举的是不同实例的情况,但是 依旧实现了同步,原因如下:

Integer存在静态缓存,范围是-128 ~ 127,当使用Integer A = 127 或者 Integer A = Integer.valueOf(127) 这样的形式,都是从此缓存拿。如果使用 Integer A = new Integer(127),每次都是一个新的对象。此例中,两个对象实例的成员变量 lockObject 其实是同一个对象,因此实现了同步。还有字符串常量池也要注意。所以此处关注是,同步代码块传参的对象是否是同一个。这跟第二个方式其实是同一种。

同步代码块传参class对象(全局锁)
  •  所有调用该方法的线程都会实现同步
//类对象锁,全局锁
public class MethodSync {

    //全局锁,类是全局唯一的
    public void test3() {
        synchronized (MethodSync.class) {
            try {
                System.out.println(Thread.currentThread().getName() + " test3 进入了同步方法");
                Thread.sleep(5000);
                System.out.println(Thread.currentThread().getName() + " test3 休眠结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " test 准备进入");
        MethodSync sync = new MethodSync();
        sync.test3();
    }
}

public class Test {
    public static void main(String[] args) {
        new MyThread().start();
        new MyThread().start();
    }
}

//Thread-0 test3 准备进入
//Thread-1 test3 准备进入
//Thread-0 test3 进入了同步方法
//Thread-0 test3 休眠结束
//Thread-1 test3 进入了同步方法
//Thread-1 test3 休眠结束
修饰静态方法(全局锁)

JLS规范里面有明确的定义static方法锁的是 Class object synchronized 修饰静态方法锁的是类对象,全局锁。

public class MethodSync {
   


    //全局锁,类是全局唯一的
    public  static synchronized void test4() {
        synchronized (MethodSync.class) {
            try {
                System.out.println(Thread.currentThread().getName() + " test4 进入了同步方法");
                Thread.sleep(5000);
                System.out.println(Thread.currentThread().getName() + " test4 休眠结束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " test 准备进入");
        MethodSync.test4();
    }
}


public class Test {
    public static void main(String[] args) {
        new MyThread().start();
        new MyThread().start();
    }
}
//Thread-0 test4 准备进入
//Thread-1 test4 准备进入
//Thread-0 test4 进入了同步方法
//Thread-0 test4 休眠结束
//Thread-1 test4 进入了同步方法
//Thread-1 test4 休眠结束

静态方法的synchronized,锁住了该方法所在的类对象上,因为一个类只能对应一个类对象,所以同时只有一个线程执行类中的静态同步方法.

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/fa8bfade7e69b607c4daad8b5
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券