关于synchronized同步的使用

rumlee 2018-03-15 10:08:56
几乎各种牛人的文章都说到单例模式的双重检查有问题,类似下面的代码

public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}

return instance;
}

并且说到有问题的原因是可能另外的线程检测到instance不为空,但是对象还没有初始化好。
但是我疑虑的是,如果这种写法有问题的话,那也就意味着在synchronized中只要new了对象都有可能有问题。
例如下面的写法

public static synchronized Object newobj(){
return new Object();
}



或者是这样的

public static synchronized void addobj(ArrayList list){
list.add(new Object());
}


那如果这些代码都有问题的话,那就无数的代码都有问题,只要是涉及到需要在多线程中创建对象的都可能有这种问题。
虽然各种大牛都说第一段代码里的这种问题,可以依靠volatile来解决。但是后面两种呢,这没法用volatile解决的啊。
感谢赐教
...全文
726 17 打赏 收藏 转发到动态 举报
写回复
用AI写文章
17 条回复
切换为时间正序
请发表友善的回复…
发表回复
cattpon 2018-03-16
  • 打赏
  • 举报
回复
http://www.importnew.com/24082.html
rumlee 2018-03-15
  • 打赏
  • 举报
回复
引用 4 楼 oyljerry 的回复:
你看到的单例的代码,其实是做了发展的,可以看到synchronized 并没有加在getInstance方法上,如果加载这个上面就不会存在问题,因为每个函数都是加锁了的,所以不会出现实例不完整的问题,也就是后面的newobj(), addobj等方法使用的。
if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null)
                    instance = new Singleton();
            }
        }
而getInstance()是所以这么做,是因为后面使用的时候,所有的函数调用都会通过单例getInstance()函数,这样就每次都需要加锁控制,因而有性能的问题。因而才发展出了如上的代码,把synchronized放到里面去加锁同步,但这个方法就会出现指令重排等问题,所以还需要加上volatile修饰的办法来避免。
从本质上讲 public static void func(){ synchronized (Singleton.class) { } } 和 public static synchronized void func(){ } 是一样的啊。
oO临时工Oo 2018-03-15
  • 打赏
  • 举报
回复
并且说到有问题的原因是可能另外的线程检测到instance不为空,但是对象还没有初始化好 这个说法本身就是错误的,所以你提出问题的前提是错误的,因此,你提出的问题不存在。 如果理清单例模式的发展,你就知道你真正想问的问题是啥 以下是最简单的单例模式
public static Singleton getInstance() {
		if (instance == null){
			instance = new Singleton();
		}

		return instance;
	}
但上面的方法有个问题,假如有两种线程thread1、thread2同时调用这个getInstance方法,且理论上存在以下可能: thread1调用方法getInstance时,在“if (instance == null){”这一行以后,在“new Singleton()”之前,由于操作系统调度(或者是JVM调度)的原因,导致线程进入wait; 在thread1重新run的时候,thread2开始执行,thread2运行好,一下就执行完了,那么thread2势必会执行“new Singleton()” 那么一定时间后,thread1重新run,由于thread1已经做过判断的,恢复run后,直接执行“new Singleton()” 鉴于上述极端情况,Singleton被new了两次。 因此,出现了以下情况的单例模式写法:
public static Singleton instance;
	private static Object instanceLock = new Object();
	public static Singleton getInstance2() {
		synchronized (instanceLock) {
			if (instance == null)
				instance = new Singleton();
		}

		return instance;
	}
但上面的方法也有一个弊端,就是每次调用getInstance2时,都会进入synchronized同步状态,这样限制了高并发时,多线程访问的速度,因此又演变成如下方案:
public static Singleton getInstance3() {
		if (instance == null) {
			synchronized (instanceLock) {
				if (instance == null)
					instance = new Singleton();
			}
		}

		return instance;
	}
这样,极端情况下,只有所有调用者在第一次调用getInstance3时,会进入同步状态,后续不会进入同步状态。
oO临时工Oo 2018-03-15
  • 打赏
  • 举报
回复
在thread1重新run的之前,thread2开始执行,thread2运行好,一下就执行完了,那么thread2势必会执行“new Singleton()”
当年我还小丶 2018-03-15
  • 打赏
  • 举报
回复
按照这种说法,java所有刚创建的对象都会有没准备好就被使用的情况 ...
oyljerry 2018-03-15
  • 打赏
  • 举报
回复
你看到的单例的代码,其实是做了发展的,可以看到synchronized 并没有加在getInstance方法上,如果加载这个上面就不会存在问题,因为每个函数都是加锁了的,所以不会出现实例不完整的问题,也就是后面的newobj(), addobj等方法使用的。
if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null)
                    instance = new Singleton();
            }
        }
而getInstance()是所以这么做,是因为后面使用的时候,所有的函数调用都会通过单例getInstance()函数,这样就每次都需要加锁控制,因而有性能的问题。因而才发展出了如上的代码,把synchronized放到里面去加锁同步,但这个方法就会出现指令重排等问题,所以还需要加上volatile修饰的办法来避免。
rumlee 2018-03-15
  • 打赏
  • 举报
回复
引用 2 楼 Ragin 的回复:
你那两个例子,连单例都算不上吧。 都是新的实例。
是的,我说的两个例子与是否单例没啥关系。但是原理一样啊。 我的意思是单例模式里面的双重检查的问题如果存在的话,那我这两个例子中没有道理不存在类似问题啊。 可以推出我的例子中,同样可能存在引用准备好了或者已经返回了,而对象没有准备好的情况。
Braska 2018-03-15
  • 打赏
  • 举报
回复
你那两个例子,连单例都算不上吧。 都是新的实例。
rumlee 2018-03-15
  • 打赏
  • 举报
回复

    public synchronized String toString() {
        int max = size() - 1;
        if (max == -1)
            return "{}";

        StringBuilder sb = new StringBuilder();
        Iterator<Map.Entry<K,V>> it = entrySet().iterator();

        sb.append('{');
        for (int i = 0; ; i++) {
            Map.Entry<K,V> e = it.next();
            K key = e.getKey();
            V value = e.getValue();
            sb.append(key   == this ? "(this Map)" : key.toString());
            sb.append('=');
            sb.append(value == this ? "(this Map)" : value.toString());

            if (i == max)
                return sb.append('}').toString();
            sb.append(", ");
        }
    }
[code=java] @Override public String toString() { // Create a copy, don't share the array return new String(value, 0, count); } [code] 这是Hashtable的toString方法和StringBuilder的toString方法的源码,HashTable的toString最后返回StringBuilder的toString,StringBuilder的toString会new一个String对象,如果按照上面问题中的说法,那有可能在return返回之后,new String对象还没有构建好,那这岂不是乱套了。对这块疑惑了很久,始终得不到一个权威的解答。
rumlee 2018-03-15
  • 打赏
  • 举报
回复
引用 15 楼 maradona1984 的回复:
[quote=引用 14 楼 rumlee 的回复:] [quote=引用 11 楼 maradona1984 的回复:] 单例的instance变量可能被多个线程访问到 你后面举的例子,变量都在线程的工作内存中,在创建过程中不管CPU如何重排指令,其他线程也访问不到这个变量,自然不存在线程安全问题
我后面举的例子当然也可以被别的线程访问到啊。

import java.util.ArrayList;
import java.util.List;

public class Test implements Runnable {
	private List<Object> list;

	public Test() {
		this.list = new ArrayList<Object>();
	}

	public static synchronized void addobj(List<Object> list) {
		list.add(new Object());
	}

	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println(this.list.get(this.list.size() - 1));
			Test.addobj(this.list);
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	public static void main(String[] args) {
		Test t = new Test();
		new Thread(t).start();
		new Thread(t).start();
	}

}
为什么说不能在多线程中访问呢? System.out.println(this.list.get(this.list.size() - 1)); Test.addobj(this.list); 这2行代码,第一行打印出来的信息有可能是其它线程写入的,按照上面说的逻辑,那完全有可能会导致引用已经写入list中去了,但是new的Object对象还没准备好的问题啊。 [/quote] 可以参考 http://www.importnew.com/27002.html 当然我在上一段回复的时候我就知道你会举这个例子,但你这个例子问题难道不是多线程环境下对同一个资源访问的问题么,这个是集合的线程安全问题了,不是你所说的那个对象的问题了,因为就算解决了你说的问题,这个集合依然存在问题,但解决集合的线程安全问题,你那个问题也就解决了 为啥单例的双重判定要加volatile,那是因为用了双重判定,如果只用一重判定,根本不需要加volatile来修饰变量 因为双重判定的第一重判定在同步块外层,没有synchronized修饰,而且由于instance = new Singleton();并非原子操作,所以存在你所说的问题 [/quote] 感谢你,在这块我感觉你确实比我理解的要深一些,通过大家的回复,我感觉对这块理解也更深入一些了。谢谢各位。
maradona1984 2018-03-15
  • 打赏
  • 举报
回复
引用 14 楼 rumlee 的回复:
[quote=引用 11 楼 maradona1984 的回复:] 单例的instance变量可能被多个线程访问到 你后面举的例子,变量都在线程的工作内存中,在创建过程中不管CPU如何重排指令,其他线程也访问不到这个变量,自然不存在线程安全问题
我后面举的例子当然也可以被别的线程访问到啊。

import java.util.ArrayList;
import java.util.List;

public class Test implements Runnable {
	private List<Object> list;

	public Test() {
		this.list = new ArrayList<Object>();
	}

	public static synchronized void addobj(List<Object> list) {
		list.add(new Object());
	}

	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println(this.list.get(this.list.size() - 1));
			Test.addobj(this.list);
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	public static void main(String[] args) {
		Test t = new Test();
		new Thread(t).start();
		new Thread(t).start();
	}

}
为什么说不能在多线程中访问呢? System.out.println(this.list.get(this.list.size() - 1)); Test.addobj(this.list); 这2行代码,第一行打印出来的信息有可能是其它线程写入的,按照上面说的逻辑,那完全有可能会导致引用已经写入list中去了,但是new的Object对象还没准备好的问题啊。 [/quote] 可以参考 http://www.importnew.com/27002.html 当然我在上一段回复的时候我就知道你会举这个例子,但你这个例子问题难道不是多线程环境下对同一个资源访问的问题么,这个是集合的线程安全问题了,不是你所说的那个对象的问题了,因为就算解决了你说的问题,这个集合依然存在问题,但解决集合的线程安全问题,你那个问题也就解决了 为啥单例的双重判定要加volatile,那是因为用了双重判定,如果只用一重判定,根本不需要加volatile来修饰变量 因为双重判定的第一重判定在同步块外层,没有synchronized修饰,而且由于instance = new Singleton();并非原子操作,所以存在你所说的问题
rumlee 2018-03-15
  • 打赏
  • 举报
回复
引用 11 楼 maradona1984 的回复:
单例的instance变量可能被多个线程访问到 你后面举的例子,变量都在线程的工作内存中,在创建过程中不管CPU如何重排指令,其他线程也访问不到这个变量,自然不存在线程安全问题
我后面举的例子当然也可以被别的线程访问到啊。

import java.util.ArrayList;
import java.util.List;

public class Test implements Runnable {
	private List<Object> list;

	public Test() {
		this.list = new ArrayList<Object>();
	}

	public static synchronized void addobj(List<Object> list) {
		list.add(new Object());
	}

	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println(this.list.get(this.list.size() - 1));
			Test.addobj(this.list);
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	public static void main(String[] args) {
		Test t = new Test();
		new Thread(t).start();
		new Thread(t).start();
	}

}
为什么说不能在多线程中访问呢? System.out.println(this.list.get(this.list.size() - 1)); Test.addobj(this.list); 这2行代码,第一行打印出来的信息有可能是其它线程写入的,按照上面说的逻辑,那完全有可能会导致引用已经写入list中去了,但是new的Object对象还没准备好的问题啊。
oyljerry 2018-03-15
  • 打赏
  • 举报
回复
引用 9 楼 oyljerry 的回复:
[quote=引用 8 楼 rumlee 的回复:] [quote=引用 4 楼 oyljerry 的回复:] 你看到的单例的代码,其实是做了发展的,可以看到synchronized 并没有加在getInstance方法上,如果加载这个上面就不会存在问题,因为每个函数都是加锁了的,所以不会出现实例不完整的问题,也就是后面的newobj(), addobj等方法使用的。
if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null)
                    instance = new Singleton();
            }
        }
而getInstance()是所以这么做,是因为后面使用的时候,所有的函数调用都会通过单例getInstance()函数,这样就每次都需要加锁控制,因而有性能的问题。因而才发展出了如上的代码,把synchronized放到里面去加锁同步,但这个方法就会出现指令重排等问题,所以还需要加上volatile修饰的办法来避免。
从本质上讲 public static void func(){ synchronized (Singleton.class) { } } 和 public static synchronized void func(){ } 是一样的啊。[/quote] 不一样,主要是调用。虽然都是调用func,但是第一种,任何线程都能进入,而第二种,只有一个能进入[/quote] 第一种方法中,主要是为了进入func以后,可以不加锁的判断是否为空,来提高并发访问的速度。但是就会带来副作用。
oO临时工Oo 2018-03-15
  • 打赏
  • 举报
回复
public static void func(){ synchronized (Singleton.class) { } } 和 public static synchronized void func(){ } 假如存在资源竞争,导致线程等待,第一个方法或比第二个方法多一个栈帧,多消耗微量的存储。
maradona1984 2018-03-15
  • 打赏
  • 举报
回复
单例的instance变量可能被多个线程访问到 你后面举的例子,变量都在线程的工作内存中,在创建过程中不管CPU如何重排指令,其他线程也访问不到这个变量,自然不存在线程安全问题
pilnyun335857183 2018-03-15
  • 打赏
  • 举报
回复
对象实例化过程的乱序写入问题?我的印象中现在的jmm应该不存在这个问题了,不记得是那个版本修复过;好像是将对象实例化过程中从内存分配、属性的初始化等都放在工作内存中进行最后同步主内存 其他线程不可能探知到变化的。先占个坑 找到详情再回复
oyljerry 2018-03-15
  • 打赏
  • 举报
回复
引用 8 楼 rumlee 的回复:
[quote=引用 4 楼 oyljerry 的回复:] 你看到的单例的代码,其实是做了发展的,可以看到synchronized 并没有加在getInstance方法上,如果加载这个上面就不会存在问题,因为每个函数都是加锁了的,所以不会出现实例不完整的问题,也就是后面的newobj(), addobj等方法使用的。
if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null)
                    instance = new Singleton();
            }
        }
而getInstance()是所以这么做,是因为后面使用的时候,所有的函数调用都会通过单例getInstance()函数,这样就每次都需要加锁控制,因而有性能的问题。因而才发展出了如上的代码,把synchronized放到里面去加锁同步,但这个方法就会出现指令重排等问题,所以还需要加上volatile修饰的办法来避免。
从本质上讲 public static void func(){ synchronized (Singleton.class) { } } 和 public static synchronized void func(){ } 是一样的啊。[/quote] 不一样,主要是调用。虽然都是调用func,但是第一种,任何线程都能进入,而第二种,只有一个能进入

62,615

社区成员

发帖
与我相关
我的任务
社区描述
Java 2 Standard Edition
社区管理员
  • Java SE
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧