现代计算机程序,基本都运行在多核多线程的环境之中,若是不懂并发编程,那么咱们编写出来的程序在多线程并发环境下极可能出现不可预知的错误,所以掌握并理解并发编程是成为一名优秀程序员的必备技能。本人将分享Java并发编程的一些知识及学习心得,主要基于对《Java并发编程实战》这本书的学习和本人平时的一些工做经验。
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。
共享:意味着变量能够被多个线程同时访问。 可变:意味着变量的值能够在其生命周期内能够发生变化。 对象的状态:能够理解为对象的属性或一组属性。 原子操做:一组操做,要么所有执行完,要么不执行。 复合操做:一组操做,必须保证执行的原子性,才能确保线程安全。 正确性:某个类的行为与其规范彻底一致,所见即所知。 竞态条件:因为不恰当的执行时序而出现不正确的结果。
正是因为许多看似原子操做的操做,其实是复合操做,而又没有加上正确的同步,才会出现竞态条件,也就是多线程并发访问产生的不正确性。例如,i++自增操做,其实是由“读取-修改-写入”这三个操做组成的复合操做,因此在多线程并发的条件下会产生不正确的结果。java
(1)无状态的对象必定是线程安全的程序员
例如没有任何属性的对象。
public class Test { public int add(int count) { return count++; } }
(2)不在线程之间共享状态变量编程
例如该对象中属性只有单线程能够访问。
(3)对象状态变量均为不可变的安全
例如咱们常常写的一些常量类,里面的属性均为final修饰的基本类型。
public class Test { public static final int count = 10; }
(4)访问对象的状态变量时使用正确的同步多线程
经过使用正确的同步机制访问对象的状态变量,java提供了synchronized、显示锁、volatile、原子变量等同步机制。一个类的状态可能由多个变量组成,那么访问这些变量的时候要使用同一个锁。例以下面的test类,该类的状态由count和hit两个变量决定,若是在修改两个变量的时候不使用同一个锁,以下所示,就会出现错误。
package com; //错误示例 public class Test { private int count; private int hit; public int addHit() { //使用Test类对象做为锁 synchronized (Test.class) { hit++; return hit; } } public int addCount() { //使用当前对象做为锁 synchronized (this) { if(hit % 2 == 0) { count++; return count; } else { return -1; } } } public static void main(String[] args) { Test test = new Test(); new Thread() { @Override public void run() { // TODO Auto-generated method stub for(int i = 0; i <= 100000; i++) { int hit = test.addHit(); int count = test.addCount(); System.out.println("线程A, hit:" + hit +" count:" + count); } } }.start(); new Thread() { @Override public void run() { // TODO Auto-generated method stub for(int i = 01; i < 100000; i++) { int hit = test.addHit(); int count = test.addCount(); System.out.println("线程B,hit:" + hit +" count:" + count); } } }.start(); } }
运行截图
正确写法:
public class Test { private int count; private int hit; public int addHit() { //线程安全,使用当前对象做为锁 synchronized (this) { hit++; return hit; } } public int addCount() { //线程安全,使用当前对象做为锁 synchronized (this) { if(hit % 2 == 0) { count++; return count; } else { return -1; } } } }
正确结果截图:
对象的状态变量是共享和可变的时候,须要使用同步机制对其进行访问。
Java提供隐式锁、可重入锁,其用法: (1)修饰一段代码块 任意java对象皆可做为锁,叫作内置锁,又叫作监视器锁,内置锁是可重入的。
public class Test { public int count; public int add(int count) { //线程安全,使用当前对象做为锁 synchronized (this) { return count++; } } }
(2)修饰实例方法 就是使用当前对象做为锁。
public class Test { public int count; public synchronized int add(int count) { //线程安全,使用当前对象做为锁 return count++; } }
(3)修饰静态方法 就是使用当前对象的类对象~~~~
public class Test { public class Test1 { private static int count; /** * 使用Test.class对象做为锁 */ public synchronized static void add() { count++; } }
(1)保证了不一样线程对这个变量进行操做时的可见性,即一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。(可见性) (2)禁止进行指令重排序。(有序性) (3)volatile 只能保证对单次读/写的原子性。
上面这几条是我在网上找到的对volatile特性的概述,具体详情本文就不详述了。可是什么时候使用volatile,只要记住一句话:当你能确保只有一个线程写变量的时候,这个变量就能够用volatile修饰,多少个线程读没有关系。volatile比直接加锁更加轻量级,因此根据场景来使用加锁仍是volatile。并发
其实在平时工做中,咱们只要能理解并发的概念和场景,而后能正确使用synchronized和volatile,就能应付大部分的并发场景了。
就是使用Java的Lock与ReentrantLock,具体本文暂不详述。
使用Java的AtomicInteger等变量,顾名思义这些变量的方法都是原子性的、线程安全的,具体本文暂不详述。