一个程序,多个线程同时操作一个变量,给这个变量+1()。功能很简单,可是怎么样去实现呢?这其中涉及到了哪些问题?
最基础想法
见代码:
public class Test extends Thread {
public static int amount = 0; public void run() {
amount++;
} public static void main(String[] args) {
int num_thread = 100;
for (int i = 0; i < num_thread; i++) {
new Test().start();
}
try {
Thread.sleep(3 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(amount);
}
}
输出结果:
num_thread = 100时,结果=100;
num_thread = 1000时,结果=1000;
num_thread = 10000时,结果=9995;
num_thread = 1000000时,结果=999936;
程序判定为不安全,当线程数比较少的时候,因为线程是先后启动的,所以看起来没有影响,一旦线程数增大,弊端毕露无疑。其实还有一个更简单看出问题的方法,线程运行时,不是给变量+1,而是+1000*1000,再来看结果:
num_thread = 10时,结果=5034021;——线程数很少,但是结果不是想要的结果。
总之说明,这样的多线程不安全!amount++这个方法并不是原子性的!
升级想法1.0:用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最新值。
见代码:
public class Test extends Thread {
public static volatile int amount = 0;//只是这里的变量声明为volatile修饰 public void run() {
int i = 0;
while (i < 1000 * 1000) {
amount++;
i++;
}
} public static void main(String[] args) {
int num_thread = 10;
for (int i = 0; i < num_thread; i++) {
new Test().start();
}
try {
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(amount);
}
}
输出结果:
num_thread = 10时,结果=2375833;——结果仍然不是想要的。
那问题出在哪里了呢?处在了对volatile修饰符的理解上。(参考博客:java中volatile关键字的含义)
volatile很容易被误用,被误用来进行原子性操作。
在 java 垃圾回收整理一文中,描述了jvm运行时刻内存的分配。其中有一个内存区域是jvm虚拟机栈,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。下面一幅图描述这种交互:
read and load ——从主存复制变量到当前工作内存
use and assign ——执行代码,改变共享变量值
store and write ——用工作内存数据刷新主存相关内容
其中use and assign 可以多次出现,但是这一些操作并不是原子性,也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样。
对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的。
这项问题搞清楚之后可以继续想法了。
升级想法3.0:同步代码块——通过 synchronized 关键字,所有加上synchronized 的块语句,在多线程访问的时候,同一时刻只能有一个线程能够用synchronized 修饰的方法或者代码块。
java为了解决线程并发的问题,在语言内部引入了 同步块 和 volatile 关键字机制。
对于synchronized的使用,又有不同的方式:同步代码块和同步方法
首先来看同步代码块的运用。语法见代码:
synchronized(syncObject){
//code
}
synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject 的锁方能执行。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。
这里面的syncObject,可以是 类实例 或 类。
针对我们的场景,修改代码如下:
public class Test extends Thread {
public static volatile Integer amount = new Integer(0);//修改为对象 public void run() {
int i = 0;
synchronized (amount) {
while (i < 1000 * 1000) {
amount++;
i++;
}
}
} public static void main(String[] args) {
int num_thread = 10;
for (int i = 0; i < num_thread; i++) {
new Test().start();
}
try {
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(amount);
}
}
变动包括,amount的类型由int变为Interger,这样amount才是一个可以被synchronized使用的Integer实例。
然而程序的输出:1902241 —— 仍然不是我们想要的。问题出在了哪里?测试发现在synchronized后面sleep 10ms 以上同步成功,sleep 1ms 的话就不会成功!!!!!!!!!!!什么鬼!!!!!!!!详情见代码注释:
public class Test implements Runnable {
// public static volatile AtomicInteger amount = new AtomicInteger(0);
private Integer amount = new Integer(0);
private InClass inClass = new InClass("adfas"); public void run() {
synchronized (amount) {
// synchronized (inClass) {// 注意!!这里换成inClass就成功了,即便没有sleep,简直郁闷啊啊啊啊啊!!!
// synchronized (this){ 或者synchronized (Test.class)都可以,即便没有sleep
System.out.println(
Thread.currentThread().getName() + "---------begin--------------" + System.currentTimeMillis());
try {
Thread.sleep(1);//sleep(10)的话可以成功,查看输出,程序输出时间确实是递增的!!!
} catch (Exception e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
for (int i = 0; i < 200000;) {
// addOne();
amount++;
i++;
}
System.out.println(
Thread.currentThread().getName() + "------------end-------------" + System.currentTimeMillis());
}
} public synchronized void addOne() {
amount++;
} public static void main(String[] args) {
int num_thread = 50;
Test test = new Test();
for (int i = 0; i < num_thread; i++) {
(new Thread(test)).start();
}
try {
Thread.sleep(10 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(test.amount);
} class InClass {
public InClass(String name) {
// TODO 自动生成的构造函数存根
}
}
}
对于上面这个例子,是我最大的疑惑。讲道理的话,amount是一个Integer的实例,我本认为synchronized(amount)锁住的是amount这个实例的同步方法或者同步代码块,按道理来说对程序是没有影响的,也就是synchronized并不起作用,但是使用inClass实例告诉我们,上面的想法是错误的(具体原因需要分析)!但是既然synchronized(syncObject)表明了可以用的话,amount抽象来看,和inClass是同一个东西,都是实例!但是amount为什么就不能用呢???感觉非常奇怪,贴一下测试结果:
测试结果1:synchronized(amount)没有sleep(),可以明显看出,代码块没有被锁住。另外,sleep(1)的时候,结果类似。
Thread-0---------begin--------------1483240909596
Thread-7---------begin--------------1483240909596
Thread-9---------begin--------------1483240909597
Thread-10---------begin--------------1483240909597
Thread-11---------begin--------------1483240909598
Thread-12---------begin--------------1483240909603
Thread-8---------begin--------------1483240909617
Thread-18---------begin--------------1483240909617
Thread-9------------end-------------1483240909628
Thread-17---------begin--------------1483240909628
Thread-10------------end-------------1483240909631
Thread-12------------end-------------1483240909631
Thread-16---------begin--------------1483240909632
Thread-20---------begin--------------1483240909632
Thread-11------------end-------------1483240909632
Thread-19---------begin--------------1483240909633
Thread-18------------end-------------1483240909636
Thread-13---------begin--------------1483240909636
Thread-14---------begin--------------1483240909632
Thread-15---------begin--------------1483240909651
Thread-48---------begin--------------1483240909651
Thread-49---------begin--------------1483240909654
Thread-20------------end-------------1483240909654
Thread-13------------end-------------1483240909655
Thread-22---------begin--------------1483240909651
Thread-30---------begin--------------1483240909651
Thread-14------------end-------------1483240909669
Thread-23---------begin--------------1483240909651
Thread-15------------end-------------1483240909670
Thread-16------------end-------------1483240909651
Thread-7------------end-------------1483240909642
Thread-31---------begin--------------1483240909651
Thread-48------------end-------------1483240909673
Thread-38---------begin--------------1483240909651
Thread-39---------begin--------------1483240909651
Thread-30------------end-------------1483240909674
Thread-46---------begin--------------1483240909651
Thread-22------------end-------------1483240909674
Thread-26---------begin--------------1483240909651
Thread-23------------end-------------1483240909686
Thread-34---------begin--------------1483240909650
Thread-42---------begin--------------1483240909650
Thread-21---------begin--------------1483240909650
Thread-25---------begin--------------1483240909650
Thread-38------------end-------------1483240909693
Thread-31------------end-------------1483240909694
Thread-28---------begin--------------1483240909650
Thread-26------------end-------------1483240909695
Thread-29---------begin--------------1483240909650
Thread-37---------begin--------------1483240909650
Thread-36---------begin--------------1483240909650
Thread-34------------end-------------1483240909705
Thread-25------------end-------------1483240909709
Thread-44---------begin--------------1483240909650
Thread-21------------end-------------1483240909711
Thread-33---------begin--------------1483240909650
Thread-28------------end-------------1483240909713
Thread-41---------begin--------------1483240909650
Thread-45---------begin--------------1483240909650
Thread-29------------end-------------1483240909713
Thread-35---------begin--------------1483240909650
Thread-42------------end-------------1483240909714
Thread-27---------begin--------------1483240909650
Thread-17------------end-------------1483240909650
Thread-24---------begin--------------1483240909650
Thread-32---------begin--------------1483240909650
Thread-44------------end-------------1483240909730
Thread-40---------begin--------------1483240909650
Thread-41------------end-------------1483240909732
Thread-35------------end-------------1483240909733
Thread-19------------end-------------1483240909650
Thread-43---------begin--------------1483240909650
Thread-0------------end-------------1483240909650
Thread-6---------begin--------------1483240909734
Thread-8------------end-------------1483240909644
Thread-45------------end-------------1483240909735
Thread-33------------end-------------1483240909733
Thread-37------------end-------------1483240909722
Thread-36------------end-------------1483240909722
Thread-39------------end-------------1483240909691
Thread-46------------end-------------1483240909691
Thread-49------------end-------------1483240909674
Thread-47---------begin--------------1483240909657
Thread-27------------end-------------1483240909740
Thread-24------------end-------------1483240909740
Thread-32------------end-------------1483240909741
Thread-40------------end-------------1483240909748
Thread-6------------end-------------1483240909751
Thread-5---------begin--------------1483240909751
Thread-43------------end-------------1483240909751
Thread-47------------end-------------1483240909751
Thread-5------------end-------------1483240909752
Thread-4---------begin--------------1483240909752
Thread-4------------end-------------1483240909754
Thread-3---------begin--------------1483240909754
Thread-3------------end-------------1483240909756
Thread-2---------begin--------------1483240909756
Thread-2------------end-------------1483240909757
Thread-1---------begin--------------1483240909757
Thread-1------------end-------------1483240909758
2589439
测试结果2:synchronized(amount)并且sleep(10),可以看出,代码是被同步了的。为了看的更清楚,sleep(1000),结果类似。
Thread-0---------begin--------------1483241058337
Thread-0------------end-------------1483241058353
Thread-49---------begin--------------1483241058353
Thread-49------------end-------------1483241058365
Thread-48---------begin--------------1483241058365
Thread-48------------end-------------1483241058378
Thread-47---------begin--------------1483241058379
Thread-47------------end-------------1483241058390
Thread-46---------begin--------------1483241058390
Thread-46------------end-------------1483241058401
Thread-45---------begin--------------1483241058401
Thread-45------------end-------------1483241058412
Thread-44---------begin--------------1483241058412
Thread-44------------end-------------1483241058423
Thread-43---------begin--------------1483241058423
Thread-43------------end-------------1483241058434
Thread-42---------begin--------------1483241058434
Thread-42------------end-------------1483241058446
Thread-41---------begin--------------1483241058446
Thread-41------------end-------------1483241058461
Thread-40---------begin--------------1483241058461
Thread-40------------end-------------1483241058472
Thread-39---------begin--------------1483241058472
Thread-39------------end-------------1483241058483
Thread-38---------begin--------------1483241058483
Thread-38------------end-------------1483241058494
Thread-37---------begin--------------1483241058494
Thread-37------------end-------------1483241058504
Thread-36---------begin--------------1483241058504
Thread-36------------end-------------1483241058515
Thread-35---------begin--------------1483241058516
Thread-35------------end-------------1483241058526
Thread-34---------begin--------------1483241058526
Thread-34------------end-------------1483241058540
Thread-33---------begin--------------1483241058541
Thread-33------------end-------------1483241058552
Thread-32---------begin--------------1483241058552
Thread-32------------end-------------1483241058563
Thread-31---------begin--------------1483241058564
Thread-31------------end-------------1483241058577
Thread-29---------begin--------------1483241058577
Thread-29------------end-------------1483241058588
Thread-30---------begin--------------1483241058588
Thread-30------------end-------------1483241058598
Thread-28---------begin--------------1483241058599
Thread-28------------end-------------1483241058610
Thread-27---------begin--------------1483241058610
Thread-27------------end-------------1483241058621
Thread-26---------begin--------------1483241058621
Thread-26------------end-------------1483241058632
Thread-25---------begin--------------1483241058632
Thread-25------------end-------------1483241058643
Thread-24---------begin--------------1483241058643
Thread-24------------end-------------1483241058654
Thread-23---------begin--------------1483241058654
Thread-23------------end-------------1483241058665
Thread-22---------begin--------------1483241058665
Thread-22------------end-------------1483241058680
Thread-21---------begin--------------1483241058680
Thread-21------------end-------------1483241058693
Thread-20---------begin--------------1483241058693
Thread-20------------end-------------1483241058706
Thread-19---------begin--------------1483241058706
Thread-19------------end-------------1483241058718
Thread-18---------begin--------------1483241058718
Thread-18------------end-------------1483241058731
Thread-17---------begin--------------1483241058731
Thread-17------------end-------------1483241058743
Thread-16---------begin--------------1483241058743
Thread-16------------end-------------1483241058757
Thread-15---------begin--------------1483241058757
Thread-15------------end-------------1483241058770
Thread-14---------begin--------------1483241058772
Thread-14------------end-------------1483241058783
Thread-13---------begin--------------1483241058783
Thread-13------------end-------------1483241058794
Thread-12---------begin--------------1483241058794
Thread-12------------end-------------1483241058805
Thread-11---------begin--------------1483241058805
Thread-11------------end-------------1483241058816
Thread-10---------begin--------------1483241058817
Thread-10------------end-------------1483241058828
Thread-9---------begin--------------1483241058828
Thread-9------------end-------------1483241058839
Thread-8---------begin--------------1483241058839
Thread-8------------end-------------1483241058850
Thread-7---------begin--------------1483241058850
Thread-7------------end-------------1483241058861
Thread-6---------begin--------------1483241058861
Thread-6------------end-------------1483241058872
Thread-3---------begin--------------1483241058872
Thread-3------------end-------------1483241058883
Thread-5---------begin--------------1483241058883
Thread-5------------end-------------1483241058895
Thread-4---------begin--------------1483241058896
Thread-4------------end-------------1483241058907
Thread-2---------begin--------------1483241058907
Thread-2------------end-------------1483241058919
Thread-1---------begin--------------1483241058919
Thread-1------------end-------------1483241058930
升级想法3.1:基于上面出现的各种问题,可以把amount++这一步直接用一个synchronized修饰的方法代替,简单明了,粗暴高效!
详见上面的代码里面的addOne()方法!但是这里其实是有问题的:毕竟真是程序中一般可能出现各种同步情况,很多时候同步代码块儿的灵活性非常好,而同步方法使用起来可能不方便;所以上面的问题还是需要解决,在网上很多的例子中我们看到的都是synchronized(this)和synchronized(Test.class),关于这两种用法的对比,见之后的补充。当我们不想锁住类的对象,只是想同步代码块的时候,可以考虑创建一个对象实例,如下图所示:
升级想法4.0:java里面有些对数变量的操作是原子性的,
Java中的原子操作包括:
1)除long和double之外的基本类型的赋值操作
2)所有引用reference的赋值操作
3)java.concurrent.Atomic.* 包中所有类的一切操作
count++不是原子操作,是3个原子操作组合
1.读取主存中的count值,赋值给一个局部成员变量tmp
2.tmp+1
3.将tmp赋值给count
方法:使用java.util.concurrent.AtomicInteger,详见代码!
import java.util.concurrent.atomic.AtomicInteger; public class Test implements Runnable {
public static volatile AtomicInteger amount = new AtomicInteger(0); public void run() {
System.out.println(
Thread.currentThread().getName() + "---------begin--------------" + System.currentTimeMillis());
for (int i = 0; i < 200000;) {
amount.incrementAndGet();
i++;
}
System.out.println(
Thread.currentThread().getName() + "------------end-------------" + System.currentTimeMillis());
}
} public static void main(String[] args) {
int num_thread = 50;
Test test = new Test();
for (int i = 0; i < num_thread; i++) {
(new Thread(test)).start();
}
try {
Thread.sleep(10 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(test.amount);
} }
另外补充!synchronized(this) VS synchronize(MyClass.class)
引用国外一个问答网站的精辟回答:
"
MyClass.class
and this
are different things, are different references to different objects.
this
- is the reference to particular this instance of class, and
MyClass.class
- is the reference to MyClass
description object.
This synchronization blocks differs in that the first will synchronize all threads that deal concretely with this instance of MyClass
, and the second one will synchronize all threads independently of which object on which this method was called.
"
翻译过来就是:this同步的是一个具体的对象,所有由这个对象产生的线程在运行同一个方法时都会被阻塞(补充:所有的synchronized标示的方法用也会被阻塞,原因是等同1);MyClass.class同步的是当前类,获取锁的方法将阻塞所有这个类的实例对象,这些对象都无权调用该方法。理解这里的this和MyClass.class非常重要!比如对于上面的例子来说,如果我们的Test extends Thread,如果每次启动线程都是new Test().start(),使用synchronized(this)是无效的(程序中amount要声明为static,属于类),因为每个线程都是一个独立的对象产生的。(注意下面两个等同)。
等同1:
public void test() {
synchronized(this) {
// todo your code
}
}
public synchronized void test() {
// todo your code
}
等同2:
如果某方法为类方法,即其修饰符为static,那么synchronized 意味着某个调用此方法的线程当前会拥有该类的锁,只要该线程持续在当前方法内运行,其他线程依然无法获得方法的使用权!
测试代码:
public class TestSynchronized {
private InClass inClass = new InClass("name"); public void test1() {
synchronized (inClass) {
int i = 5;
while (i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
} public synchronized void test2() { int i = 5;
while (i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
} } public void test3() {
synchronized (TestSynchronized.class) {
int i = 5;
while (i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
} public void test4() {
synchronized (this) {
int i = 5;
while (i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
}
}
} public synchronized void test5() { int i = 5;
while (i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
}
} } public static void main(String[] args) {
final TestSynchronized myt1 = new TestSynchronized();
final TestSynchronized myt2 = new TestSynchronized();
Thread test1 = new Thread(new Runnable() {
public void run() {
myt1.test2();
}
}, "test1");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
Thread test2 = new Thread(new Runnable() {
public void run() {
myt1.test5();
}
}, "test2");
test1.start();
test2.start();
} class InClass {
public InClass(String name) {
// TODO 自动生成的构造函数存根
}
}
}
测试会发现,synchronized(this)会锁住代码块本身的方法、synchronized标示的方法和其它synchroized(this)的代码块;而synchronized(MyClass.class)只能锁住不同对象对应的这一个方法块儿!其他方法(即便是同步的)不会被锁住!
纸上得来终觉浅,还盼诸君勤实践啊!!!啊啊啊啊啊啊!!!!!
参考:synchronize类锁和对象锁详解;深入理解java中的synchronized关键字;java中volatile关键字的含义(这个的评论要看一下)
-------------------------------------- 我 是 华 丽 的 分 割 线 ------------------------------------------
--->>>原因找到了!
关键在于amount is an instance of Integer,synchronized锁的是amount这个对象,但是for循环中amount++这个操作会使得amount这个对象发生变化,这个通过hashcode可以看出来,对于instance of Integer,amount.intValue()==amount.hashcode().所以当这个对象变了之后,之前对象的锁自然就没用了,其它线程开始竞争新对象的锁,由此造成了这样的结果。
--->>>关于synchronized一点补充
synchronized的实现机制需要参照jvm,了解的知识比较多。synchronized(syncObject)类似于给某个对象加锁,{\\code}代码块里面的东西在执行之前都需要获取这把锁,运行完之后马上释放锁。