1、线程安全
在多线程系统中,当多个线程共享同一份资源时,会出现线程不安全的情况。我们可以使用关键字synchronized加锁来解决线程不安全的问题。
1.1 同步方法
在方法上加上synchronized关键字,普通同步方法默认的锁对象是当前对象this,对于静态同步方法,锁是当前类的Class对象。
public synchronized void test2(){
//方法体
}
1.2 同步方法块
用关键字synchronized修饰可能存在线程不安全的代码块,锁对象是我们指定的引用类型的对象,基本类型不能作为锁。
synchronized(锁对象){
//可能存在线程不安全的代码
}
1.3 以售票讲解线程同步
public class SynDemo01 {
/**
* @param args
*/
public static void main(String[] args) {
//真实角色
Web12306 web= new Web12306();
//代理
Thread t1 =new Thread(web,"路人甲");
Thread t2 =new Thread(web,"黄牛已");
Thread t3 =new Thread(web,"攻城师");
//启动线程
t1.start();
t2.start();
t3.start();
}
}
/**
* 线程安全的类
*/
class Web12306 implements Runnable {
private int num =10;
private boolean flag =true;
@Override
public void run() {
while(flag){
test5();
}
}
public void test6(){
if(num<=0){
flag=false; //跳出循环
return ;
}
//a b c
synchronized(this){
try {
Thread.sleep(500); //模拟 延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
}
}
//线程不安全 锁定资源不正确
public void test5(){
//a b c
synchronized((Integer)num){//以num作为对象,可以理解为大门没关严实,比如里面的flag都还没有关闭,所以这属于锁对象不正确导致线程不安全
if(num<=0){
flag=false; //跳出循环
return ;
}
try {
Thread.sleep(500); //模拟 延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
}
}
//锁定范围不正确 线程不安全
public void test4(){
// c 1
synchronized(this){
//b
if(num<=0){
flag=false; //跳出循环
return ;
}
}
// b
try {
Thread.sleep(500); //模拟 延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
}//a -->1
//线程安全 锁定正确
public void test3(){
//a b c
synchronized(this){//也是以this作为锁,相当于是大门
if(num<=0){
flag=false; //跳出循环
return ;
}
try {
Thread.sleep(500); //模拟 延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
}
}
//线程安全 同步方法,锁对象是this
public synchronized void test2(){
if(num<=0){
flag=false; //跳出循环
return ;
}
try {
Thread.sleep(500); //模拟 延时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
}
//线程不安全
public void test1(){
if(num<=0){
flag=false; //跳出循环
return ;
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到了"+num--);
}
}
2、从单例设计模式认识线程同步
2.1 最简单的懒汉式单例
懒汉式单例模式设计步骤:
1 私有构造器 2 声明私有的静态实例对象 3 提供静态的访问该对象的方法
代码如下:
public class Jvm{
private static Jvm instance = null;
private Jvm(){}
public static Jvm getInstance(){
if(instance==null){
instance = new Jvm();
}
return instance;
}
}
测试单线程的时候,单例模式创建的多个对象是否是同一个对象:
public class JvmTest {
public static void main(String[] args) {
//单线程下创建两个Jvm对象
Jvm instance1 = Jvm.getInstance();
Jvm instance2 = Jvm.getInstance();
System.out.println("两个对象是否相等:"+(instance1 == instance2));//两个对象是否相等:true
}
}
由此可知,单线程的时候,这种简单的懒汉式单例模式是线程安全的。
2.2 多线程下,线程不安全的懒汉式
2.2.1 单例模式2
public class Jvm2 {
private static Jvm2 instance = null;
private Jvm2(){}
public static Jvm2 getInstance(long time){
if(instance==null){
try {
Thread.sleep(time);//模拟线程阻塞,增加错误概率
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Jvm2();
}
return instance;
}
}
2.2.2 创建线程类
public class JvmThread extends Thread{
private long time;//创建对象的延迟时间,只是用来放大错误的
public JvmThread(){
}
public JvmThread(long time){
this.time = time;
}
@Override
public void run() {
//调用Jvm得到实例对象
System.out.println(Thread.currentThread().getName()+"-->创建:"+Jvm2.getInstance(time));
}
}
2.2.3 测试多线程下单例模式得到的对象不是同一个
public static void main(String[] args) {
JvmThread jvmThread1 = new JvmThread(500);
JvmThread jvmThread2 = new JvmThread(100);
jvmThread1.start();
jvmThread2.start();
}
控制台输出:
Thread-1-->创建:org.dt.framework.util.Jvm2@3cf5b814
Thread-0-->创建:org.dt.framework.util.Jvm2@28084850
可以看到这种懒汉式的单例模式在多线程下是线程不安全的,创建了多个对象。
2.3 线程安全,效率低的懒汉式单例
因为上面的单例模式线程不安全,所以我们可以加上同步,使其线程安全,
2.3.1 修改Jvm2为Jvm3:
public class Jvm3 {
private static Jvm3 instance = null;
private Jvm3(){}
public static Jvm3 getInstance(long time){
synchronized (Jvm3.class) {//加上锁,并且静态方法中,锁对象是该类的class文件
if(instance==null){
try {
Thread.sleep(time);//模拟线程阻塞,增加错误概率
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new Jvm3();
}
return instance;
}
}
}
2.3.2 修改线程类
修改线程类,调用的是Jvm3的单例:
public class JvmThread extends Thread{
private long time;//创建对象的延迟时间,只是用来放大错误的
public JvmThread(){
}
public JvmThread(long time){
this.time = time;
}
@Override
public void run() {
//调用Jvm得到实例对象,这里改成了Jvm3
System.out.println(Thread.currentThread().getName()+"-->创建:"+Jvm3.getInstance(time));
}
}
运行上面的测试类,控制台打印:
Thread-0-->创建:org.dt.framework.util.Jvm3@8523ca2
Thread-1-->创建:org.dt.framework.util.Jvm3@8523ca2
可以看到,此时是线程安全的,多线程下我们得到的是同一个对象。
2.4 线程安全,效率高的懒汉式
2.3的懒汉式单例已经是线程安全的,但是效率比较低。因为如果有多个线程访问它想得到对象,都要在synchronized外进行等待,获取锁对象,然后判断对象是否存在。获取锁对象是一个比较耗时的操作,多此获取锁对象会降低效率。那么我们如何优化呢?
其实优化方式就是减少等待。所以我们可以在synchronized外再加上一层判断,只有当第一次的时候,对象不存在的时候才获取锁对象,创建实例。第二次以后,我们判断的时候,对象已经不为null,直接返回对象即可。
2.4.1 修改Jvm3为Jvm4
//双重检查的懒汉式单例
public class Jvm4 {
private static Jvm4 instance = null;
private Jvm4(){}
public static Jvm4 getInstance(long time){
if(instance == null){
synchronized (Jvm4.class) {//加上锁,并且静态方法中,锁对象是该类的class文件
if(instance==null){
instance = new Jvm4();
}
}
}
return instance;
}
}
这个也成为双重检查的懒汉式单例设计模式。
3、饿汉式的单例设计模式
3.1 常见的饿汉式单例
public class Jvm {
//声明的时候就实例化,就不会存在线程不安全的情况,因为只要类一加载,就会实例化
private static Jvm instance = new Jvm();
private Jvm(){}
public static Jvm getInstance(){
return instance;
}
}
该饿汉式单例模式是线程安全的,它的缺点就是 可能创建对象的时机比较早,只要类加载就会被创建,也许此时我们还不需要该对象。比如这个类中还有其它的方法,当我们调用其他方法的时候,类加载,该单例对象会被创建。如果我们需要在真正需要使用该对象的时候在创建,该怎么办呢?
3.2 优化饿汉式单例模式
public class Jvm2 {
private static class Jvm2Holder{
private static Jvm2 instance = new Jvm2();
}
private Jvm2(){}
public static Jvm2 getInstance(){
return Jvm2Holder.instance;
}
}