首先介绍可见性、原子性、有序性、重排序这几个概念 

原子性:即一个操作或多个操作要么全部执行并且执行的过程不会被任何因素打断,要么都不执行。

可见性:一个线程对共享变量值的修改,能够及时地被其它线程看到

  共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量

  每个线程都有自己的工作内存,存有主内存中共享变量的副本,当工作内存中的共享变量改变,会主动刷新到主内存中,其它工作内存要使用共享变量时先从主内存中刷新共享变量到工作内存,这样就保证了共享变量的可见性。

  

可见性的实现方法:

  1、synchronized两条规则

    线程解锁前,必须要把共享变量的最新值刷新到主内存中

    线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(加锁与解锁需要是同一把锁)

    总之,线程解锁前对共享变量的修改在下次加锁时对其他线程可见。

  2、volatile

    只能保证内存的可见性,不能保证操作的原子性。

有序性:即程序执行的顺序按照代码的先后顺序执行。

    在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

    重排序:代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化

    1、编译器优化的重排序(编译器优化)

    2、指令集并行重排序(处理器优化)

    3、内存系统重排序(处理器优化)

    as-if-serial:无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致(java编译器、运行时和处理器都会保证java在单线程下遵循as-if-serial语义)

    重排序不会给单线程带来内存可见性问题

    多线程中程序交错执行,重排序可能会造成内存可见性问题

线程不可见的原因:

    1、线程的交叉执行(需要原子性)

    2、重排序结合线程交叉执行(需要原子性)

    3、共享变量更新后的值没有在工作内存与贮存间及时更新(需要内存可见性)

1、线程同步

  线程同步是保证多个线程安全访问竞争资源的一种手段。

  1.1、通过synchronized关键字(修饰方法、代码块)

  synchronized保证锁内操作的原子性,内存的可见性。

  县城执行互斥代码的过程:获取互斥锁、清空工作内存、从住内存中拷贝最新变量到工作内存、执行代码、将更改后的共享变量的值刷新到住内存、释放互斥锁。

public class SynchronizedTest {
public static void main(String[] args) {
final Outputter output = new Outputter();
new Thread() {
public void run() {
output.output("Thread ");
}
}.start();
new Thread() {
public void run() {
output.output("synchronized ");
}
}.start();
}
} class Outputter {
public synchronized void output(String name) {
/*synchronized (this) {*/
for (int i = 0; i < name.length(); i++) {
System.out.print(name.charAt(i));
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/*}*/
}
}

  上述代码可以保证两个单词不被拆分,但不能保证其顺序,通过join方法可实现顺序输出。如果去掉synchronized两个单词将被拆分输出。

  注意:当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)代码块

       当一个线程访问object的一个synchronized(this)同步代码块时,其它线程对object中所有其它synchronized(this)代码块的访问将会被阻塞。

     当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。

  1.2、通过域变量(volatile)实现线程同步(变量)

  volatile能够保证内存的可见性,不能保证操作的原子性。

  volatile确保变量每次使用的时候是从主存中获取,而不是每个线程各自的工作内存,volatile具有synchronized关键字的“可见性”,但是没有synchronized关键字的“并发正确性”(因为线程对共享资源的读写不具有原子性),也就是说不保证线程执行的有序性。

  volatile实现内存的可见性是通过内存屏障和禁止重排序优化来实现的,对volatile变量执行写操作时,会在写操作后加入一条store屏障指令(刷新变量到主内存),对volatile变量执行读操作时会在都操作前加入一条load屏障指令(重主内存中读取变量)。

public class VolatileDemo {

    private volatile int number = 0;

    public int getNumber() {
return number;
} public void increase(){
this.number++;//不是原子操作,number++相当于3步
System.out.println(number);
} public static void main(String[] args) {
final VolatileDemo vd = new VolatileDemo();
for(int i = 0; i < 500; i++) {
new Thread(new Runnable() {
public void run() {
vd.increase();
}
}).start();
}
    //保证所有线程执行完成再执行输出
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("number : " + vd.getNumber());
} }

  上述代码的输出结果很多时候都小于500,这是由于number++的非原子性操作导致的。也就是说A线程从主内存read到number修改后还没load到主内存中,这时B线程从主内存中也read到number,导致主内存中number有时候会被覆盖掉,因而输出结果会有小于500的情况。

  为了保证上述number++的原子性,可使用synchronized、ReentrantLock(java.util.concurrent.locks包下)、AtomicInterger(java.util.concurrent.atomic包下)三种方式实现。

  synchronized实现同步上面已介绍,这里不再累述。

  ReentrantLock方式如下:

public class VolatileDemo {
private Lock lock = new ReentrantLock();
private volatile int number = 0; public int getNumber() {
return number;
} public void increase(){
lock.lock();
try {
this.number++;//不是原子操作,number++相当于3步
}finally {
lock.unlock();//保证锁的释放
}
System.out.println(number);
} public static void main(String[] args) {
final VolatileDemo vd = new VolatileDemo();
for(int i = 0; i < 500; i++) {
new Thread(new Runnable() {
public void run() {
vd.increase();
}
}).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("number : " + vd.getNumber());
} }

  运行上述代码,我们发现结果为确定的500。

  注意:共享数据的访问权限都必须定义为private

     java中没有提供检测与避免死锁的专门机制,但应用程序员可以采用某些策略防止死锁的发生

     java中对共享数据操作的并发控制是采用加锁技术

  1.3、通过重入锁实现线程同步

    参考1.2中volatile原子性问题解决办法ReentrantLock方式的代码。

  1.4、通过局部变量实现线程同步

  ava.lang.ThreadLocal,线程局部变量,把一个共享变量变为一个线程的私有对象。不同线程访问一个ThreadLocal类的对象时,锁访问和修改的事每个线程变量各自独立的对象。通过ThreadLocal可以快速把一个非线程安全的对象转换成线程安全的对象。(同时也就不能达到数据传递的作用了)。引用代码

public class BankTest {
public class Bank{
//使用ThreadLocal类管理共享变量account
private ThreadLocal<Integer> account = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue(){
return 100;
}
};
public void save(int money){
account.set(account.get()+money);
}
public int getAccount(){
return account.get();
}
} class NewThread implements Runnable {
private Bank bank; public NewThread(Bank bank) {
this.bank = bank;
} public void run() {
for (int i = 0; i < 10; i++) {
// bank.save1(10);
bank.save(10);
System.out.println(i + "账户余额为:" + bank.getAccount());
}
} } /**
* 建立线程,调用内部类
*/
public void useThread() {
Bank bank = new Bank();
NewThread new_thread = new NewThread(bank);
System.out.println("线程1");
Thread thread1 = new Thread(new_thread);
thread1.start();
System.out.println("线程2");
Thread thread2 = new Thread(new_thread);
thread2.start();
} public static void main(String[] args) {
BankTest st = new BankTest();
st.useThread();
} }

  1.5、通过阻塞队列实现线程同步

  前面5种同步方式都是在底层实现的线程同步,但是我们在实际开发当中,应当尽量远离底层结构。 使用javaSE5.0版本中新增的java.util.concurrent包将有助于简化开发。本小节主要是使用LinkedBlockingQueue<E>来实现线程的同步。(引用代码)

public class BlockingSynchronizedThread {
/**
* 定义一个阻塞队列用来存储生产出来的商品
*/
private LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<Integer>();
/**
* 定义生产商品个数
*/
private static final int size = 10;
/**
* 定义启动线程的标志,为0时,启动生产商品的线程;为1时,启动消费商品的线程
*/
private int flag = 0; private class LinkBlockThread implements Runnable {
public void run() {
int new_flag = flag++;
System.out.println("启动线程 " + new_flag);
if (new_flag == 0) {
for (int i = 0; i < size; i++) {
int b = new Random().nextInt(255);
System.out.println("生产商品:" + b + "号");
try {
queue.put(b);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("仓库中还有商品:" + queue.size() + "个");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} else {
for (int i = 0; i < size / 2; i++) {
try {
int n = queue.take();
System.out.println("消费者买去了" + n + "号商品");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("仓库中还有商品:" + queue.size() + "个");
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
} public static void main(String[] args) {
BlockingSynchronizedThread bst = new BlockingSynchronizedThread();
LinkBlockThread lbt = bst.new LinkBlockThread();
Thread thread1 = new Thread(lbt);
Thread thread2 = new Thread(lbt);
thread1.start();
thread2.start(); }

  1.6、通过原子变量实现线程同步

  需要使用线程同步的根本原因在于对普通变量的操作不是原子的,util.concurrent.atomic包中提供了创建了原子类型变量的工具类AtomicInteger 表可以用原子方式更新int的值。

public class VolatileDemo {
private AtomicInteger number = new AtomicInteger(0); public AtomicInteger getNumber() {
return number;
} public synchronized void increase(){
number.addAndGet(1);
System.out.println(number);
} public static void main(String[] args) {
final VolatileDemo vd = new VolatileDemo();
for(int i = 0; i < 500; i++) {
new Thread(new Runnable() {
public void run() {
vd.increase();
}
}).start();
}
//保证所有线程执行完成再执行输出
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("number : " + vd.getNumber());
}
}

  上述代码输出结果为确定的500。

2、数据交换

  由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和return语句来返回数据。

  1)通过构造方法传递数据

public class ParaTest extends Thread{

    private String name;

    @Override
public void run() {
super.run();
System.out.println(Thread.currentThread().getName() + " | " + name);
} public ParaTest(String name){
this.name = name;
} public static void main(String[] args) {
ParaTest p0 = new ParaTest("thread0");
Thread t0 = new Thread(p0, "thread0");
t0.start();
ParaTest p1 = new ParaTest("thread1");
Thread t1 = new Thread(p1, "thread1");
t1.start();
}
}

  当传递数据过多时,构造方法会显得特别臃肿,因此可以使用变量方法的方式。

  2)通过变量和方法传递数据

public class ParaTest extends Thread{

    private String names;

    public String getNames() {
return names;
} public void setNames(String names) {
this.names = names;
} @Override
public void run() {
super.run();
System.out.println(Thread.currentThread().getName() + " | " + getNames());
} public static void main(String[] args) {
ParaTest p0 = new ParaTest();
p0.setNames("thread0");
Thread t0 = new Thread(p0, "thread0");
t0.start();
ParaTest p1 = new ParaTest();
p1.setNames("thread1");
Thread t1 = new Thread(p1, "thread1");
t1.start();
}
}

  3)通过回调函数传递数据

  上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是main方法中主动将数据传入线程类的。然而,在有些应用中需要在线程运行的过程中动态地获取数据,这种情况可以使用回调函数方式。

public class ParaTest extends Thread{

    private String names;
private int age; public String getNames() {
return names;
} public void setNames(String names) {
this.names = names;
} public int getAge() {
return age;
} public void setAge(int age) {
this.age = age;
} public void progress(String name, int age) {
System.out.println(name + " | " + age);
} @Override
public void run() {
progress(getNames(), getAge());
} public static void main(String[] args) {
ParaTest p0 = new ParaTest();
p0.setNames("thread0");
p0.setAge(0);
Thread t0 = new Thread(p0, "thread0");
t0.start();
ParaTest p1 = new ParaTest();
p1.setNames("thread1");
p1.setAge(1);
Thread t1 = new Thread(p1, "thread1");
t1.start();
}
}

3、线程死锁

  所谓死锁: 是指两个或两个以上的进程(线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外部处理作用,它们都将无限等待下去。

  产生原因:

    1、系统资源不足,导致线程对资源的竞争引起

    2、进程的推进顺序不恰当

    3、资源分配不当

  死锁产生的条件:

    1、互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

    2、请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

    3、不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

    4、环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

  一个死锁的例子

public class DeadLockTest  implements Runnable {
private int flag = 1;
private static Object obj1 = new Object(), obj2 = new Object(); public void run() {
System.out.println("flag=" + flag);
if (flag == 1) {
synchronized (obj1) {
System.out.println("我已经锁定obj1,休息0.5秒后锁定obj2去!");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (obj2) {
System.out.println("我已经锁定obj2,休息0.5秒后锁定obj1去!");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj1) {
System.out.println("0");
}
}
}
} public static void main(String[] args) {
DeadLockTest run01 = new DeadLockTest();
DeadLockTest run02 = new DeadLockTest();
run01.flag = 1;
run02.flag = 0;
Thread thread01 = new Thread(run01);
Thread thread02 = new Thread(run02);
System.out.println("线程开始喽!");
thread01.start();
thread02.start();
}
}

  解决死锁的办法

  1、预防死锁:设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个

  2、避免死锁:而是在资源的动态分配过程中,用某种方法去防止系统进入不安全状态

  3、检测和解除死锁:先检测再解除。此方法允许系统在运行过程中发生死锁,但可通过系统所设置的检测机构(检测方法包括定时检测、效率低时检测、进程等待时检测等。),及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源,采取适当措施,从系统中将已发生的死锁清除掉。

4、synchronized和volatile的比较

  volatile不需要加锁,比synchronized更轻量级,不会阻塞线程

  从内存可见性角度讲,volatile读相当于加锁,写相当于解锁

  synchronized即能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性

Java多线程学习(吐血超详细总结)

Java线程面试题 Top 50

java并发之原子性、可见性、有序性

java笔记--关于线程同步(7种同步方式)

java从基础知识(十)java多线程(下)的更多相关文章

  1. 什么才是java的基础知识?

    近日里,很多人邀请我回答各种j2ee开发的初级问题,我无一都强调java初学者要先扎实自己的基础知识,那什么才是java的基础知识?又怎么样才算掌握了java的基础知识呢?这个问题还真值得仔细思考. ...

  2. JAVA相关基础知识

    JAVA相关基础知识 1.面向对象的特征有哪些方面 1.抽象: 抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面.抽象并不打算了解全部问题,而只是选择其中的一部分, ...

  3. java必备基础知识(一)

    学习的一点建议: 每一门语言的学习都要从基础知识开始,学习是一个过程,"万丈高楼平地起",没有一个好的地基,想必再豪华的高楼大厦终究有一天会倒塌.因此,我们学习知识也要打牢根基,厚 ...

  4. Java SE 基础知识(一)

    一.基础知识 1. Java SE : Java Standard Edition Java ME : Java Micro Edition Java EE : Java Enterprise Edi ...

  5. java部分基础知识整理----百度脑图版

    近期发现,通过百度脑图可以很好的归纳总结和整理知识点,本着学习和复习的目的,梳理了一下java部分的知识点,不定期更新,若有不恰之处,请指正,谢谢! 脑图链接如下:java部分基础知识整理----百度 ...

  6. java从基础知识(十)java多线程(上)

    线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元.另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点 ...

  7. Java基础知识强化之多线程笔记01:多线程基础知识(详见Android(java)笔记61~76)

    1. 基础知识: Android(java)学习笔记61:多线程程序的引入    ~    Android(java)学习笔记76:多线程-定时器概述和使用 

  8. Java基础知识强化之多线程笔记06:Lock接口 (区别于Synchronized块)

    1. 简介 我们讲到了如何使用关键字synchronized来实现同步访问.本文我们继续来探讨这个问题,从Java 5之后,在java.util.concurrent.locks包下提供了另外一种方式 ...

  9. Java基础知识强化之多线程笔记05:Java程序运行原理 和 JVM的启动是多线程的吗

    1. Java程序运行原理:     Java 命令会启动Java 虚拟机,启动 JVM,等于启动了一个应用程序,也就是启动了一个进程.该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 m ...

  10. 集合框架、泛型、迭代(java基础知识十六)

    1.ArrayList存储自定义对象并遍历 此类的 iterator 和 listIterator 方法返回的迭代器是快速失败的:在创建迭代器之后,除非通过迭代器自身的 remove 或 add 方法 ...

随机推荐

  1. 同步降压DC-DC转换IC——XC9264

    设计一个12V转3.3V,输出电流30mA的电源电路,由于项目对转化效率要求较高,所以不能采用低压差线性稳压LDO的方案.经过对比,TOREX的XC9264效率在此转化条件下效率可做到85%以上,比M ...

  2. Linux 系统常用命令汇总(三) 用户和用户组管理

    用户和用户组管理 命令 选项 注解 示例 useradd [选项] 用户名 新建用户 创建一个名为tester的用户,并指定他的UID为555,指定加入test群,指定其使用C-shell:  use ...

  3. BZOJ 1717: [Usaco2006 Dec]Milk Patterns 产奶的模式 [后缀数组]

    1717: [Usaco2006 Dec]Milk Patterns 产奶的模式 Time Limit: 5 Sec  Memory Limit: 64 MBSubmit: 1017  Solved: ...

  4. Java程序设计求出岁数

    题目:我年龄的立方是个4位数.我年龄的4次方是个6位数.这10个数字正好包含了从0到9这10个数字,每个都恰好出现1次,求出我今年几岁. 直接拷贝运行就可以了. public class Age { ...

  5. asp.net中缓存的使用介绍一

    asp.net中缓存的使用介绍一 介绍: 在我解释cache管理机制时,首先让我阐明下一个观念:IE下面的数据管理.每个人都会用不同的方法去解决如何在IE在管理数据.有的会提到用状态管理,有的提到的c ...

  6. C++ 使用ifstream读取数据,多读最后一行问题解决方法

    C++文件读取时有一个bug,就是使用eof()判断文件结尾并不准确,最后一行会重复读取一次,可采用以下方法避免重复读取: while (!inFile.eof()) { inFile >> ...

  7. AppBoxPro - 细粒度通用权限管理框架(可控制表格行内按钮)源码提供下载

    特别声明: 提供的源代码已经包含了 AppBoxPro 的全部源代码,用 VS2012 打开项目后,直接 Ctrl+F5 可以运行起来(默认使用VS自带的LocalDB数据库). FineUIPro是 ...

  8. kubernetes 1.4.5集群部署

    2016/11/16 23:39:58 环境: centos7 [fu@centos server]$ uname -a Linux centos 3.10.0-327.el7.x86_64 #1 S ...

  9. c风格字符串

    1.字符数组截取 有当然有了,应均包含在<string.h>中. 有strncpy,strncat.可以帮你从任何位置,取得任意合法长度的字符串. 用法基本同strcpy,strcat. ...

  10. layer.open打开iframe页面的调用父页面方法及关闭

    //调用父类方法 window.parent.exportData($('#shownum').val(),$('#splitstr').val()); //关闭iframe页面var index = ...