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

一、多线程三大特性

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

原子性

(跟数据库的事务特性中的原子性类似,数据库的原子性体现是dml语句执行后需要进行提交):
理解:即一个操作或多个操作,要么全部执行并且执行的过程中不会被任何因素打断,要么都不执行。
一个很经典的例子就是银行账户转账问题:
比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。这2个操作必须要具备原子性才能保证不出现一些意外的问题。
我们操作数据也是如此,比如i = i+1;其中就包括,读取i的值,计算i,写入i。这行代码在Java中是不具备原子性的,则多线程运行肯定会出问题,所以也需要我们使用同步synchronized和lock锁这些东西来确保这个特性了。
原子性其实就是保证数据一致、线程安全一部分,

可见性:

可见性是与java内存模型息息相关的。
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程2没有看到,这就是可见性问题。

有序性:

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

例如:

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,因为这打破了依赖关系。
显然重排序对单线程运行是不会有任何问题,而多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。
多线程中保证有序性的方法:join()

二、Java内存模型

jvm的内存结构为:堆、栈、方法区,不同于java的内存模型,Java的内存模型是关于多线程相关的。

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

总结:什么是Java内存模型:java内存模型简称jmm,定义了一个线程对另一个线程可见。共享变量存放在主内存中,每个线程都有自己的本地内存,当多个线程同时访问一个数据的时候,可能本地内存没有及时刷新到主内存,所以就会发生线程安全问题。

三、Volatile关键字

Volatile关键字的作用:变量在多个线程之间可见。

Volatile关键字是非原子性的,不能保证数据的原子性,只是能够把解决立马刷新到主内存中,不能解决并发问题。

如果想要保证数据的原子性,解决并发问题,需要使用并发包里的AutomicInteger原子类。

volatile与synchronized区别:
仅靠volatile不能保证线程的安全性(原子性)。

  1. 1.volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法。
  2. 2.volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。

synchronized不仅保证可见性,而且还保证原子性,因为只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时会出现阻塞。

synchronized会把主内存中的共享变量锁住,永远只有一个线程操作主内存的共享变量。

线程安全性包括两个方便:1.可见性 2.原子性

仅仅使用volatile不能保证线程安全性,而synchronized则可实现线程的安全性。

代码实现:

package chauncy.concurrentprogramming;

class ThreadVolatile extends Thread {
	public volatile boolean flag = true;

	@Override
	public void run() {
		System.out.println("子线程开始执行...");
		while (flag) {

		}
		System.out.println("子线程结束执行...");
	}

	public void isRun(boolean flag) {
		this.flag = flag;
	}
}

/**
 * @classDesc: 功能描述(Volatile关键字的使用)
 * @author: ChauncyWang
 * @createTime: 2019年3月12日 上午10:17:14
 * @version: 1.0
 */
public class Volatile {
	public static void main(String[] args) throws InterruptedException {
		ThreadVolatile threadVolatile1 = new ThreadVolatile();
		threadVolatile1.start();
		Thread.sleep(300);
		/**
		 * 如果不对变量加Volatile关键字,则子线程不会停止运行 原因:线程之间是不可见的,读取的是副本,没有及时读取到主内存结果。
		 * 解决办法:使用Volatile关键字解决线程之间的可见性,强制线程每次读取该值的时候都去“主内存”中取值。
		 */
		threadVolatile1.isRun(false);
		System.out.println("flag:" + threadVolatile1.flag);
	}
}
package chauncy.concurrentprogramming;

import java.util.concurrent.atomic.AtomicInteger;

class VolatileNoAtomicThread extends Thread {
	// private static volatile int count = 0;
	private static AtomicInteger atomicInteger = new AtomicInteger(0);

	@Override
	public void run() {
		for (int i = 0; i < 1000; i++) {
			// count++;
			atomicInteger.incrementAndGet();// count++
		}
		System.out.println(getName() + "-----" + atomicInteger);
	}
}

/**
 * @classDesc: 功能描述(Volatile修饰不具有原子性(不具有同步性),不能解决线程安全问题)
 * @author: ChauncyWang
 * @createTime: 2019年3月12日 上午10:39:30
 * @version: 1.0
 */
public class VolatileNoAtomic {
	public static void main(String[] args) {
		// 初始化10个线程
		VolatileNoAtomicThread[] volatileNoAtomicThread = new VolatileNoAtomicThread[10];
		for (int i = 0; i < volatileNoAtomicThread.length; i++) {
			// 创建每一个线程
			volatileNoAtomicThread[i] = new VolatileNoAtomicThread();
		}
		for (int i = 0; i < volatileNoAtomicThread.length; i++) {
			// 启动每一个线程
			volatileNoAtomicThread[i].start();
		}
	}
}

四、TreadLocal

1.什么是ThreadLocal?

ThreadLocal提高一个线程的局部变量,访问某个线程拥有自己局部变量。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程对应的副本。

ThreadLocal接口方法有4个:

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

2.ThreadLocal底层实现原理:

ThreadLocal通过Thread.currentThread();获取当前线程

操作map集合:ThreadLocalMap

void set(Object value)就是Map.put(“当前线程”,值);

public Object get()就是获取ThreadLocalMap然后操作后返回。

代码实现:

package chauncy.concurrentprogramming;

class Res {
	// private int count=0;
	/*
	 * 设置本地局部变量,和其他线程局部变量隔离开,互不影响
	 */
	private ThreadLocal<Integer> count = new ThreadLocal<Integer>() {
		protected Integer initialValue() {
			// 设置当前线程局部变量的初始化值
			return 0;
		};
	};

	/**
	 *
	 * @methodDesc: 功能描述(生成订单号)
	 * @author: ChauncyWang
	 * @param: @return
	 * @createTime: 2019年3月12日 下午2:23:57
	 * @returnType: int
	 */
	public Integer getNum() {
		int count = this.count.get() + 1;
		this.count.set(count);
		return count;
	}
}

class ThreadLocalDemo extends Thread {
	private Res res;

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

	@Override
	public void run() {
		for (int i = 0; i < 3; i++) {
			try {
				Thread.sleep(30);
			} catch (Exception e) {
			}
			System.out.println(getName() + "----i:" + i + ",number:" + res.getNum());
		}
	}
}

/**
 * @classDesc: 功能描述(本地线程的使用:创建三个线程,每个线程生成自己独立的序列号)
 * @author: ChauncyWang
 * @createTime: 2019年3月12日 下午2:21:03
 * @version: 1.0
 */
public class ThreadLocalTest {
	public static void main(String[] args) {
		Res res = new Res();
		ThreadLocalDemo t1 = new ThreadLocalDemo(res);
		ThreadLocalDemo t2 = new ThreadLocalDemo(res);
		ThreadLocalDemo t3 = new ThreadLocalDemo(res);
		t1.start();
		t2.start();
		t3.start();
	}
}

五、线程池

1.为什么要使用线程池?

因为要通过线程池来管理线程,启动或者停止一个线程非常耗费资源,所以将线程交给线程池来管理能够节约内存。
一般在企业开发当中我们都使用线程池,通过spring去整合线程池,异步注解。

2.什么是线程池?

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

3.线程池作用:

基于以下几个原因,在多线程应用程序中使用线程池是必须的:

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

4.线程池四种创建方式:

java通过Executors(jdk1.5的并发包)提供四种线程池,分别为:

  1. 1.newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  2. 2.newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  3. 3.newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行
  4. 4.newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。(一般不会使用)

总结:newCachedThreadPool 创建的线程,线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。newFixedThreadPool 每次执行传入参数大小个线程,其他线程在等待(企业中用的不多)。newScheduledThreadPool 使用schedule方法创建单位时间的延迟线程池。

代码实现:

package chauncy.concurrentprogramming.executors;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NewCachedThreadPool {
	public static void main(String[] args) {
		// 创建可缓存线程池
		ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
		// 执行execute方法表示创建了一个线程,类似于start
		for (int i = 0; i < 30; i++) {
			int index = i;
			// index++;
			newCachedThreadPool.execute(new Runnable() {

				@Override
				public void run() {
					try {
						Thread.sleep(300);
					} catch (InterruptedException e) {
					}
					// 内部类中使用的i必须是final,但是换成index后就不报错,因为jdk1.8进行了优化,能识别index是否被改变,如果把int
					// index=i;下边的index++放开就会报错。
					System.out.println(Thread.currentThread().getName() + "----" + index);
				}
			});
		}
		// 关闭线程池
		newCachedThreadPool.shutdown();
	}
}
package chauncy.concurrentprogramming.executors;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NewFixedThreadPool {
	public static void main(String[] args) {
		// newFixedThreadPool 每次最多只能执行三个,其他线程等待执行。
		ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
		for (int i = 0; i < 10; i++) {
			int index = i;
			newFixedThreadPool.execute(new Runnable() {
				public void run() {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
					}
					System.out.println(Thread.currentThread().getName() + "----i:" + index);
				}
			});
		}
	}
}
package chauncy.concurrentprogramming.executors;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class NewScheduledThreadPool {
	public static void main(String[] args) {
		// 入参为线程池大小,
		ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
		// schedule执行定时任务线程池,第一个参数需要创建Runnable接口对象,第二、三个参数表示多少个单位时间执行run方法。
		newScheduledThreadPool.schedule(new Runnable() {
			public void run() {
				System.out.println("我是三秒钟之后执行。。。。");
			}
		}, 3, TimeUnit.SECONDS);
	}
}
package chauncy.concurrentprogramming.executors;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class NewSingleThreadExecutor {
	public static void main(String[] args) {
		ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
		for (int i = 0; i < 10; i++) {
			int index = i;
			newSingleThreadExecutor.execute(new Runnable() {
				public void run() {
					System.out.println(Thread.currentThread().getName() + "----i:" + index);
				}
			});
		}
	}
}

以上所述是小编给大家介绍的Java多线程与并发编程详解整合,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

时间: 2019-03-22

详解Java中Thread 和Runnable区别

Thread 和Runnable 关系 Thread类是接口Runnable的一个实现类. public class Thread implements Runnable 源码分析 Thread Threa类运行的时候调用start()方法,源代码如下: 调用start()方法,实际运行的是start0方法,方法声明如下: private native void start0() native表明这个方法是个原生函数,即这个函数是用C/C++实现的,被编译成DLL,由Java调用. native

Java ThreadLocal的设计理念与作用

Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量.因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的ThreadLocal变量. 如何创建ThreadLocal变量 以下代码展示了如何创建一个ThreadLocal变量: private ThreadLocal myThreadLocal = new ThreadLocal(); 我们可以看到,通过这段代码实例化了一个ThreadLocal对象.我们只需要实例

浅谈java String不可变的好处

一.java内部String类的实现: java 8: public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; } java 9 及之后:(使用coder标识了编码) public final class Stri

详解JAVA中的Collection接口和其主要实现的类

Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements).一些Collection允许相同的元素而另一些不行.一些能排序而另一些不行.Java SDK不提供直接继承自Collection的类,Java SDK提供的类都是继承自Collection的"子接口"如List和Set,详细信息可见官方文档http://tool.oschina.net/uploads/apidocs/jdk-zh/java/util/

Javascript的this详解

在理解javascript的this之前,首先先了解一下作用域. 作用域分为两种: 1.词法作用域:引擎在当前作用域或者嵌套的子作用域查找具有名称标识符的变量.(引擎如何查找和在哪查找.定义过程发生在代码书写阶段) 2.动态作用域:在运行时被动态确定的作用域. 词法作用域和动态作用域的区别是:词法作用域是在写代码或定义时确定的:动态作用域是在运行时确定的. this的绑定规则 this是在调用时被绑定,取决于函数的调用位置.由此可以知道,一般情况下(非严格模式下),this都会根据函数调用(调用

java_IO向文件中写入和读取内容代码实例

使用java中OutStream()向文件中写入内容 package Stream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; public class OutStreamDemo01 { public static void main(Str

Java双重检查加锁单例模式的详解

什么是DCL DCL(Double-checked locking)被设计成支持延迟加载,当一个对象直到真正需要时才实例化: class SomeClass { private Resource resource = null; public Resource getResource() { if (resource == null) resource = new Resource(); return resource; } } 为什么需要推迟初始化?可能创建对象是一个昂贵的操作,有时在已知的运

4位吸血鬼数字的java实现思路与实例讲解

这个问题来源于Java编程思想一书,所谓"吸血鬼数字"就是指位数为偶数的数字,可以由一对数字相乘而得到,而这对数字各包含乘积的一半位数字,其中从偶数位数字中选取的数字可以任意排列.例如: 1260=21*60,1827=21*87,2187=27*81-- 先列出结果: 一共7个: 1260=21*60,1395=15*93,1435=41*35,1530=51*30,1827=87*21,2187=27*81,6880=86*80 第一种思路对所有的4位数进行穷举,假设这个4位数是a

Java中缀表达式转后缀表达式实现方法详解

本文实例讲述了Java中缀表达式转后缀表达式实现方法.分享给大家供大家参考,具体如下: 本文先给出思路与方法,最后将给出完整代码 项目实战: https://www.jb51.net/article/158335.htm 算法综述: 一.中缀表达式转后缀表达式: 1.中缀表达式要转后缀表达式,首先需要两个Stack(栈),其中一个应用于存放字符,另一个用于存放数字. 2.读到数字直接存入数字栈中,读到字符时,要咸鱼栈内前一元素(字符)进行比较,当当前(要存入的字符)优先级大于迁移字符时才存入,否

详解Java包装类及自动装箱拆箱

Java包装类 基本类型 大小 包装器类型 boolean / Boolean char 16bit Boolean byte 8bit Byte short /16bit Short int 32bit Integer long 64bit Long float 32bit Float double 64bit Double void / Void Java 的包装类有两个主要的目的: Java包装类将基本数据类型的值"包装"到对象中,对基本数据类型的操作变为了对对象进行操作,从而使

java自动装箱拆箱深入剖析

这个是jdk1.5以后才引入的新的内容,作为秉承发表是最好的记忆,毅然决定还是用一篇博客来代替我的记忆: java语言规范中说道:在许多情况下包装与解包装是由编译器自行完成的(在这种情况下包装成为装箱,解包装称为拆箱): 其实按照我自己的理解自动装箱就可以简单的理解为将基本数据类型封装为对象类型,来符合java的面向对象:例如用int来举例: 复制代码 代码如下: //声明一个Integer对象 Integer num = 10; //以上的声明就是用到了自动的装箱:解析为 Integer nu

Java Integer及int装箱拆箱对比

示例代码: class BoxIntInteger { public static void main(String[] args) { Integer a = new Integer(10111); int b = 10111; boolean equal1 = a == b; boolean equal2 = a.equals(b); System.out.println(equal1); System.out.println(equal2); } } 反编译字节码: public stat

详解Java中int和Integer的区别

基本数据类型和引用类型 Java是面向对象的编程语言,一切都是对象,但是为了编程的方便还是引入了基本数据类型,为了能够将这些基本数据类型当成对象操作,Java为每一个基本数据类型都引入了对应的包装类型(wrapper class),int的包装类就是Integer,从Java 5开始引入了自动装箱/拆箱机制,使得二者可以相互转换,对应如下: 原始类型:boolean,char,byte,short,int,long,float,double 包装类型:Boolean,Character,Byte

详解Java 自动装箱与自动拆箱

包装器 有些时候,我们需要把类似于int,double这样的基本数据类型转成对象,于是设计者就给每一个基本数据类型都配置了一个对应的类,这些类被称为包装器. 包装器整体来说分为四大种: Number,Number类派生出了Integer,Double,Long,Float,Short,Byte这六个小类分别代表了int,double,long,float,short,byte这六种基本数据类型. Character,对应的基本数据类型是char. Void,对应的是关键字void,这个类我们会经

详解Java的堆内存与栈内存的存储机制

堆与内存优化     今天测了一个项目的数据自动整理功能,对数据库中几万条记录及图片进行整理操作,运行接近到最后,爆出了java.lang.outOfMemoryError,java heap space方面的错误,以前写程序很少遇到这种内存上的错误,因为java有垃圾回收器机制,就一直没太关注.今天上网找了点资料,在此基础上做了个整理.  一.堆和栈 堆-用new建立,垃圾回收器负责回收 1.程序开始运行时,JVM从OS获取一些内存,部分是堆内存.堆内存通常在存储地址的底层,向上排列. 2.堆

详解Java 10 var关键字和示例教程

关键要点 Java 10引入了一个闪亮的新功能:局部变量类型推断.对于局部变量,现在可以使用特殊的保留类型名称"var"代替实际类型. 提供这个特性是为了增强Java语言,并将类型推断扩展到局部变量的声明上.这样可以减少板代码,同时仍然保留Java的编译时类型检查. 由于编译器需要通过检查赋值等式右侧(RHS)来推断var的实际类型,因此在某些情况下,这个特性具有局限性,例如在初始化Array和Stream的时候. 如何使用新的"var"来减少样板代码. 在本文中,

详解Java线程池和Executor原理的分析

详解Java线程池和Executor原理的分析 线程池作用与基本知识 在开始之前,我们先来讨论下"线程池"这个概念."线程池",顾名思义就是一个线程缓存.它是一个或者多个线程的集合,用户可以把需要执行的任务简单地扔给线程池,而不用过多的纠结与执行的细节.那么线程池有哪些作用?或者说与直接用Thread相比,有什么优势?我简单总结了以下几点: 减小线程创建和销毁带来的消耗 对于Java Thread的实现,我在前面的一篇blog中进行了分析.Java Thread与内

详解 Java继承关系下的构造方法调用

详解 Java继承关系下的构造方法调用 在Java中创建一个类的对象时,如果该类存在父类,则先调用父类的构造方法,然后再调用子类的构造方法.如果父类没有定义构造方法,则调用编译器自动创建的不带参数的默认构造方法.如果父类定义了public的无参的构造方法,则在调用子类的构造方法前会自动先调用该无参的构造方法.如果父类只有有参的构造方法,没有无参的构造方法,则子类必须在构造方法中必须显式调用super(参数列表)来指定某个有参的构造方法.如果父类定义有无参的构造方法,但无参的构造方法声明为priv

详解Java中HashSet和TreeSet的区别

详解Java中HashSet和TreeSet的区别 1. HashSet HashSet有以下特点: 不能保证元素的排列顺序,顺序有可能发生变化 不是同步的 集合元素可以是null,但只能放入一个null 当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置. 简单的说,HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个