多线程同步机制(线程安全)
- 程序的是否线程安全:该程序的方法是不是同步方法。在以后查看某一个类的源码的时候,如果该类中的方法是同步方法,那么表示该类是线程安全的。(当该类在进行操作的时候不会被其他操作影响)
- 线程安全其实就是为了保证数据的安全.
- 对于程序来说,要保证多个线程在去进行同一个数据的操作的时候,数据要保证一致。需要的结果和运行实际结果是一致的。
- 线程不安全:指的是多线程进行同一个数据的操作时,数据是不一致的。 尤其是对数据进行写操作(修改)。
- 使用runnable创建线程类,多个thread线程其实是同时在对同一个runnable线程对象在进行操作。 最终run方法是在操作同一个runnable对象。 无论属性是静态的还是非静态 ,都可以保证操作的是同一个数据(同一个对象的内存中的属性值)只有在第一次抢占的时候会导致数据冲突.
- 静态和非静态属性的结果是一样的
- 使用的是thread创建线程
- 非静态属性
- 静态属性
**如何解决这些问题:**使用锁的机制,synchronized关键字和lock锁。
代码块,静态代码,构造方法(构造器)
- 代码块:实例化对象的时候执行,每实例化一次执行一次
- 静态代码:静态代码块 类被加载的时候执行,只执行一次
- 构造方法:实例化对象的时候执行
一.synchronized关键字
- 使用同步机制:互斥锁,排他锁,悲观锁。
1.同步代码块
- synchronized(锁定的对象){ run方法中的具体操作}:同步代码快使用同一个对象作为锁来使用,同一个对象可以是任意对象(使用this关键字) 同一个对象同一个锁
- synchronized(new Object)是不同的对象
public class Ticket01 implements Runnable {
int num=10;
//创建同一对象
Object lock=new Object();
@Override
public void run() {
// TODO Auto-generated method stub
//使用同步代码块,参数是同一个对象,常用方法是使用this去做
synchronized(lock) {
boolean flag=true;
while(flag) {
if(this.num>0) {
System.out.println(Thread.currentThread().getName()+"现在还剩"+this.num+"张票");
this.num--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else {
flag=false;
}
}
}
}
}
2.同步方法
- 使用synchronized 关键字来修改方法。
- Ticket03.java
public class Ticket03 implements Runnable {
int num=10;
@Override
public void run() {
// TODO Auto-generated method stub
ticket();
}
//同步方法
public synchronized void ticket() {
boolean flag=true;
while(flag) {
if(this.num>0) {
System.out.println(Thread.currentThread().getName()+"现在还剩"+this.num+"张票");
this.num--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else {
flag=false;
}
}
}
}
- 主程序
public class Demo03 {
public static void main(String[] args) {
//实例化线程对象
Ticket03 tk=new Ticket03();
Thread t01=new Thread(tk,"t01");
Thread t02=new Thread(tk,"t02");
Thread t03=new Thread(tk,"t03");
//启动线程
t01.start();
t02.start();
t03.start();
}
}
3.静态同步方法
- 使用synchronized关键字来修改静态方法,使用的反射的思想来获取同一个对象.
public class Ticket02 implements Runnable {
static int num = 10;
// 创建同一对象
Object lock = new Object();
// 执行状态
@Override
public void run() {
// TODO Auto-generated method stub
ticket();
}
//同步方法,底层使用java的反射思想,this属于对象所有,不属于类
public static synchronized void ticket() {
boolean flag = true;
while (flag) {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "窗口卖出的是第" + num + "张票");
num--;//先读后写
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else {
flag = false;
}
}
}
}
3.1注意要避免死锁问题
- 被锁定的两个对象之间进行相互调用,就会出现死锁。
public class Ticket02 implements Runnable {
int num = 10;
final Object locka = new Object();
final Object lockb = new Object();
// 随机数
Random rd = new Random();
// 执行状态
@Override
public void run() {
// 获取随机数
int num = rd.nextInt(2);
if(num%2==0) {
synchronized (locka) {
System.out.println("a对象锁定");
synchronized (lockb) {
System.out.println("我要使用lockb");
}
}
}else {
synchronized (lockb) {
System.out.println("b对象锁定");
synchronized (locka) {
System.out.println("我要使用locka");
}
}
}
}
}
二.Lock锁的方式称为互斥锁或可重入锁
1. 在需要加锁的地方加锁,加完锁在将锁释放掉
步骤:
- 创建锁Lock,ReentrantLock;
- 使用完记得释放锁;
- 在线程的run方法中,加了多少回锁就需要释放多少次锁。
2.Lock常用方法
返回值 | 方法 | 含义 |
---|---|---|
void | lock() | 获取锁定。 |
void | lockInterruptibly() | 如果当前线程未被中断,则获取锁定。 |
Condition | newCondition() | 返回绑定到此 Lock 实例的新 Condition 实例。 |
boolean | tryLock() | 仅在调用时锁定为空闲状态才获取该锁定。 |
boolean | tryLock(long time, TimeUnit unit) | 如果锁定在给定的等待时间内空闲,并且当前线程未被中断,则获取锁定。 |
void | unlock() | 释放锁定。 |
3.ReentrantLock类
3.1ReentrantLock构造方法
构造方法 | 含义 |
---|---|
ReentrantLock() | 创建一个 ReentrantLock 的实例。 |
ReentrantLock(boolean fair) | 创建一个具有给定公平策略的 ReentrantLock。 |
3.1ReentrantLock常用方法
返回值 | 方法 | 含义 |
---|---|---|
int | getHoldCount() | 查询当前线程保持此锁定的次数。 |
protected Thread | getOwner() | 返回目前拥有此锁定的线程,如果此锁定不被任何线程拥有,则返回 null。 |
protected Collection | getQueuedThreads() | 返回一个 collection,它包含可能正等待获取此锁定的线程。 |
int | getQueueLength() | 返回正等待获取此锁定的线程估计数。 |
protected Collection | getWaitingThreads(Condition condition) | 返回一个 collection,它包含可能正在等待与此锁定相关给定条件的那些线程。 |
int | getWaitQueueLength(Condition condition) | 返回等待与此锁定相关的给定条件的线程估计数。 |
boolean | hasQueuedThread(Thread thread) | 查询给定线程是否正在等待获取此锁定。 |
boolean | hasQueuedThreads() | 查询是否有些线程正在等待获取此锁定。 |
boolean | hasWaiters(Condition condition) | 查询是否有些线程正在等待与此锁定有关的给定条件。 |
boolean | isFair() | 如果此锁定的公平设置为 true,则返回 true。 |
boolean | isHeldByCurrentThread() | 查询当前线程是否保持此锁定。 |
boolean | isLocked() | 查询此锁定是否由任意线程保持。 |
void | lock() | 获取锁定。 |
void | lockInterruptibly() | 如果当前线程未被中断,则获取锁定。 |
Condition | newCondition() | 返回用来与此 Lock 实例一起使用的 Condition 实例。 |
String | toString() | 返回标识此锁定及其锁定状态的字符串。 |
boolean | tryLock() | 仅在调用时锁定未被另一个线程保持的情况下,才获取该锁定。 |
boolean | tryLock(long timeout, TimeUnit unit) | 如果锁定在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁定。 |
void | unlock() | 试图释放此锁定。 |
- LockDemo.java
public class LockDemo implements Runnable {
//共享资源
static int num = 10;
//创建锁对象
ReentrantLock lock01=new ReentrantLock();
ReentrantLock lock02=new ReentrantLock();
// 执行状态
@Override
public void run() {
boolean flag=true;
while(flag) {
//加锁
lock01.lock();
//lock01.lock();
if(this.num>0) {
System.out.println(Thread.currentThread().getName()+"现在还剩"+this.num+"张票");
this.num--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else {
flag=false;
}
System.out.println("锁的次数"+lock01.getHoldCount());
System.out.println("估计数"+lock01.getQueueLength());
System.out.println("是不是公平锁"+lock01.isFair());
System.out.println("是不是由任意线程所有"+lock01.isLocked());
//释放锁
lock01.unlock();
//lock01.lock();//锁一下
}
}
}
- 主程序Demo
public class Demo {
public static void main(String[] args) {
//实例化线程对象
LockDemo tk=new LockDemo();
Thread t01=new Thread(tk,"t01");
Thread t02=new Thread(tk,"t02");
//启动线程
t01.start();
t02.start();
}
}
三.volatile关键字
- 主要使用在属性上
public class VolatileDemo extends Thread{
//声明变量
volatile boolean flag=false;//标记 程序可以正常停止
//boolean flag=false;//标记 程序无法停止
int i=0;//数字
//线程的run方法
@Override
public void run() {
//循环进行i++
while(!this.flag) {
i++;
}
}
public static void main(String[] args) {
//创建线程对象
VolatileDemo vd=new VolatileDemo();
//启动线程
vd.start();
try {
//主程序休息2秒钟
Thread.sleep(2000);
//修改主程序标记位
vd.flag=true;
//输出i的值
System.out.println("i的值为"+vd.i);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
当标记flag不使用volatile关键字描述的时候,线程无法停止,使用volatile关键字修饰之后可以正常停止 。
1.Java的JMM结构(java内存模型)
- Java平台自动集成了线程以及多处理器技术,这种集成程度比Java以前诞生的计算机语言要厉害很多,该语言针对多种异构平台的平台独立性而使用的多线程技术支持也是具有开拓性的一面,有时候在开发Java同步和线程安全要求很严格的程序时,往往容易混淆的一个概念就是内存模型。究竟什么是内存模型?内存模型描述了程序中各个变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节,对象最终是存储在内存里面的,这点没有错,但是编译器、运行库、处理器或者系统缓存可以有特权在变量指定内存位置存储或者取出变量的值。【JMM】(Java Memory Model的缩写)允许编译器和缓存以数据在处理器特定的缓存(或寄存器)和主存之间移动的次序拥有重要的特权,除非程序员使用了volatile或synchronized明确请求了某些可见性的保证。(百度百科)
Java语言规范中提到过,JVM中存在一个主存区(Main Memory或Java Heap Memory)Java中所有变量都是存在主存中的
- 每个线程又存在自己的工作内存,工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作并非发生在主存区,而是发生在工作内存中而线程之间是不能直接相互访问,变量在程序中的传递,是依赖主存来完成的。
- flag不使用volatile,我们所有的操作是对线程内存中的flag进行读取, vt.flag = true;是对主内存区的变量的值进行的修改,对线程对象内存是不可见。
- 使用volatile 修饰的属性,是主内存中的变量,当进行直接的赋值,修改的是主内存中的变量值。
2.Java内存模型的特点 :
(1)原子性(Atomicity):
这一点说明了该模型定义的规则针对原子级别的内容存在独立的影响,对于模型设计最初,这些规则需要说明的仅仅是最简单的读取和存储单元写入的的一些操作,这种原子级别的包括——实例、静态变量、数组元素,只是在该规则中不包括方法中的局部变量。
public class VolatileTest01 {
volatile int i;
public void addI(){
i++;
}
public static void main(String[] args) throws InterruptedException {
final VolatileTest01 test01 = new VolatileTest01();
for (int n = 0; n < 1000; n++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
test01.addI();
}
}).start();
}
Thread.sleep(10000);//等待10秒,保证上面程序执行完成
System.out.println(test01.i);
}
}
(2)可见性(Visibility):
在该规则的约束下,定义了一个线程在哪种情况下可以访问另外一个线程或者影响另外一个线程,从JVM的操作上讲包括了从另外一个线程的可见区域读取相关数据以及将数据写入到另外一个线程内。
public class VolatileTest {
int a = 1;
int b = 2;
public void change(){
a = 3;
b = a;
}
public void print(){
System.out.println("b="+b+";a="+a);
}
public static void main(String[] args) {
while (true){
final VolatileTest test = new VolatileTest();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
}
- 为什么会出现b=3;a=1这种结果呢?正常情况下,如果先执行change方法,再执行print方法,输出结果应该为b=3;a=3。相反,如果先执行的print方法,再执行change方法,结果应该是 b=2;a=1。那b=3;a=1的结果是怎么出来的?原因就是第一个线程将值a=3修改后,但是对第二个线程是不可见的,所以才出现这一结果。如果将a和b都改成volatile类型的变量再执行,则再也不会出现b=3;a=1的结果了。
(3)可排序性(Ordering):
该规则将会约束任何一个违背了规则调用的线程在操作过程中的一些顺序,排序问题主要围绕了读取、写入和赋值语句有关的序列。
public class Singleton {
public static volatile Singleton singleton;
/**
* 构造函数私有,禁止外部实例化
*/
private Singleton() {};
public static Singleton getInstance() {
if (singleton == null) {
synchronized (singleton) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
3.volatile的原理
- volatile可以实现变量的资源共享(考虑到主内存中的数据),volatile修饰的变量需要考虑该变量的原子性、可见性 (只有单一的读取或者修改才能保证)。
- volatile不能替代synchronized或者 lock锁的操作。 在必要的时候需要将Volatile和synchronized结合使用。
- 主内存和线程内存之间的数据可见性和原子性.