程序员必会的Java多线程与并发编程

1、线程三大特性

多线程有三大特性:原子性、可见性、有序性

原子性:
即一个操作或者多个操作,要么全部执行成功,要么全都不执行。
一个很经典的例子就是银行账户转账问题:
比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。这2个操作都必须要具备原子性才能保证转账成功,而不会出现一些意外的情况。

可见性:
当多个线程访问同一个变量时,如果一个线程修改了这个变量的值,其他线程能够立即看得到修改后的值。
就拿 i=i+1 举例:若两个线程在不同的cpu,如果线程1改变了 i 的值,但是没有及时刷新到主存中,线程2又使用了 i ,那么这个 i 的值肯定还是之前的,线程2没有及时看到线程1对变量 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内存模型

Java内存模型(简称JMM),也称为共享内存模型。JMM决定一个线程对共享变量的写入时,能对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
在这里插入图片描述
从上图来看,线程A与线程B之间如果要进行通信的话,必须要经历下面2个步骤:

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

下面通过示意图来说明这两个步骤:
在这里插入图片描述
如上图所示,本地内存A和B有主内存中共享变量x的副本。假设初始时,这三个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。 JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

总结:java内存模型简称JMM,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。用于解决当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存而引发的线程安全问题。

3、Volatile

volatile 关键字的作用就是让变量在多个线程之间可见。

下面我们使用代码来进行演示:

public class ThreadVolatileDemo  extends Thread{
	public  boolean flag = true;
	
	@Override
	public void run() {
		System.out.println("子线程开始执行....");
		while (flag) {
		}
		System.out.println("子线程结束执行....");
	}
	
	public void setFlag(boolean flag) {
		this.flag = flag;
	}
	
	public static void main(String[] args) throws InterruptedException {
		ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
		threadVolatileDemo.start();
		Thread.sleep(600);
		threadVolatileDemo.setFlag(false);
		System.out.println(threadVolatileDemo.flag);
	}
}

运行结果:
在这里插入图片描述
在上面的代码中,我们明明已经将结果设置为fasle,但是为什么还一直在运行呢?
原因:因为线程之间是不可见的,读取的是本地的副本,没有及时读取到主内存中的最新变量值。
解决办法:使用volatile关键字可以解决线程之间的可见性, 强制线程每次读取该值的时候都去“主内存”中取值,这样可以保证每次读取到的都是变量的最新值。

我们只需要在变量前面加上 volatile 就可以使变量在多个线程之间可见,如下所示:

public volatile boolean flag = true;

加上 volatile 之后的运行结果为:
在这里插入图片描述
此时while循环结束,表明已经读取到了主存中的最新值。

使用 volatile 时,虽然每个线程都会去内存中读取最新的变量值,但是 volatile 不具备原子性,每个线程读取变量的时候可能其他线程并没有执行完毕,因此可能会引发线程安全问题。

下面我们使用代码来演示一下:

public class VolatileAtomicityTest {
    private static final int THREADS_CONUT = 20;
    public static volatile int count = 0;
    
    public static void increase() {
        count++;
    }
    
   public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_CONUT];
        for (int i = 0; i < THREADS_CONUT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                       increase();
                    }
                }
            });
            
            threads[i].start();
        }
        
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        
        System.out.println(count);
    }
}

运行结果如下:
在这里插入图片描述
如果 volatile 具备原子性,那么运行结果应该为20000。此时的运行结果明显不对,说明 volatile 不具备原子性。

那么怎样解决原子性问题呢?

答案就是使用AtomicInteger原子类

public class AtomicIntegerTest {
    private static final int THREADS_CONUT = 20;
    public static AtomicInteger count = new AtomicInteger(0);
    
    public static void increase() {
        count.incrementAndGet();
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_CONUT];
        for (int i = 0; i < THREADS_CONUT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                       increase();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        System.out.println(count);
    }
}

此时的运行结果为:
在这里插入图片描述
说明 AtomicInteger 可以解决原子性问题。

volatile 与 synchronized 的区别

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

4、ThreadLocal(本地线程)

Synchronized用于线程间的数据共享,而ThreadLocal 则主要用于线程间的数据隔离。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

ThreadLocal底层通过Map集合实现,以线程作为key,泛型作为value,可以理解为线程级别的缓存。每一个线程都会获得一个单独的Map。

ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:

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

案例:创建三个线程,每个线程生成自己独立序列号。

class Res {
	// 生成序列号共享变量
	public static Integer count = 0;
	public static ThreadLocal<Integer> threadLocal = 
		new ThreadLocal<Integer>() { //初始化
		protected Integer initialValue() {
			return 0;
		};
	};
	
	public Integer getNum() {
		int count = threadLocal.get() + 1;
		threadLocal.set(count);
		return count;
	}
}

public class ThreadLocaDemo extends Thread {
	private Res res;

	public ThreadLocaDemo(Res res) {
		this.res = res;
	}

	@Override
	public void run() {
		for (int i = 0; i < 3; i++) {
			System.out.println(Thread.currentThread().getName() + "---" + "num:" + res.getNum());
		}
	}

	public static void main(String[] args) {
		Res res = new Res();
		ThreadLocaDemo t1 = new ThreadLocaDemo(res);
		ThreadLocaDemo t2 = new ThreadLocaDemo(res);
		ThreadLocaDemo t3 = new ThreadLocaDemo(res);
		t1.start();
		t2.start();
		t3.start();
	}
}

5、线程池

线程池是指在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程。线程池中线程的数量通常完全取决于可用内存数量和应用程序的需求。线程池中的每个线程都会被分配一个任务,一旦任务完成之后,线程就会回到线程池中并等待下一次分配任务。

线程池的作用:

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

Java通过 Executors 提供了四种线程池,分别为:

  1. newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若没有可回收的线程时,则新建线程;
  2. newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待;
  3. newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行;
  4. newSingleThreadExecutor 创建一个单线程的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
发布了21 篇原创文章 · 获赞 295 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/Mr_wxc/article/details/105752937