在java多线程并发操作中,如果不加任何的同步控制,有可能会出现一些错误的情况。
package com.lql.thread;
public class MyTask10 implements Runnable {
private int n = 10;
public MyTask10() {
}
public void method(){
while (n > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->"
+ n--);
}
}
@Override
public void run() {
// TODO Auto-generated method stub
method();
}
public static void main(String[] args) {
MyTask10 task10 = new MyTask10();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(task10);
thread.start();
}
}
}
一次运行结果:
Thread-1-->10
Thread-2-->9
Thread-0-->8
Thread-3-->9
Thread-4-->7
Thread-0-->6
Thread-2-->4
Thread-1-->5
Thread-3-->6
Thread-4-->3
Thread-3-->1
Thread-2-->-1
Thread-0-->2
Thread-1-->0
Thread-4-->-2
可以看到一些数字打印了两遍,一些打印了一遍,更为要命的是还打印出来一些负数。如果学习过操作系统并发的知识的话,这个问题其实不难理解。JVM为每个线程分配时间片,并选择了一个线程将处理机分配给他。但是这个线程执行执行到sleep()时进入了休眠状态(也就是阻塞态),这时JVM就会再选择一个线程分配处理机资源。设想如果当n=1时,有三个线程都进入了都执行到Thread.sleep(10),当这三个线程被唤醒使就会去执行打印语句部分。因为他们都已经经历了n>0条件,所以都会执行打印语句,而且执行n--,就可能出现上述出现-1,-2的情况。打印两次也是这样,当一个线程执行完打印n,还没来得及对n进行减一,就被剥夺了处理机。另一个线程此时又打印了一次n。这样的情况在并发编程中是不允许的,就比如买火车票,上述情况就如同有的票被卖给两个人,没有票了还在售卖。
java中提供了同步关键字:synchrnoized(同步)来解决多线程并发操作中的由于共享资源导致的资源冲突。
package com.lql.thread;
public class MyTask10 implements Runnable {
private int n = 10;
public MyTask10() {
}
public synchronized void method(){
while (n > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->"
+ n--);
}
}
@Override
public void run() {
// TODO Auto-generated method stub
method();
}
public static void main(String[] args) {
MyTask10 task10 = new MyTask10();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(task10);
thread.start();
}
}
}
运行结果:
Thread-0-->10
Thread-0-->9
Thread-0-->8
Thread-0-->7
Thread-0-->6
Thread-0-->5
Thread-0-->4
Thread-0-->3
Thread-0-->2
Thread-0-->1
在方法前加一个synchronized关键字,该方法就变成了一个同步方法,就不会出现上一个那样错误的情况了。但是又有一个奇怪的情况:在主线程中我开启了五个线程去执行task10这个任务,为什么只有打印出来的线程名只有一个Thread-0呢。这里就得说明一下synchronized同步的机制了。在java中每个对象都有一个对象锁,一个对象的对象锁同一时间只能由一个线程获取。一个线程从获取该对象锁的时刻起到该线程释放该对象锁止,其他线程是无法取得该对象锁的。如果线程无法获取该对象的对象锁,那么这些线程是无法去执行该对象的同步方法的。例子的运行结果就是因为:Thread-0取得了task10这个对象的对象锁,他就一直占用着task10对象的对象锁。另外四个线程只能干巴巴的看着,进不了被同步的方法里去。
我在同步方法之前和之后又加了两个普通的输出语句,来说明对象锁对于同步代码块的锁定。
package com.lql.thread;
public class MyTask10 implements Runnable {
private int n = 10;
public MyTask10() {
}
public synchronized void method(){
while (n > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->"
+ n--);
}
}
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(Thread.currentThread().getName() + "hello");
method();
System.out.println(Thread.currentThread().getName()+"hello");
}
public static void main(String[] args) {
MyTask10 task10 = new MyTask10();
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(task10);
thread.start();
}
}
}
运行结果:
Thread-1hello
Thread-3hello
Thread-0hello
Thread-4hello
Thread-2hello
Thread-1-->10
Thread-1-->9
Thread-1-->8
Thread-1-->7
Thread-1-->6
Thread-1-->5
Thread-1-->4
Thread-1-->3
Thread-1-->2
Thread-1-->1
Thread-1hello
Thread-2hello
Thread-4hello
Thread-0hello
Thread-3hello
可以看到确实开启了五个线程,而且同步方法里的代码只有一个线程在执行(而且这次是Thread-1占用了对象锁),执行完同步方法里的代码后,其他线程这时又登场了。
我们再来看一种情况。
package com.lql.thread;
public class MyTask10 implements Runnable {
private int n = 10;
public MyTask10() {
}
public synchronized void method(){
while (n > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->"
+ n--);
}
}
@Override
public void run() {
// TODO Auto-generated method stub
method();
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new MyTask10());
thread.start();
}
}
}
Thread-0-->10
Thread-3-->10
Thread-2-->10
Thread-1-->10
Thread-4-->10
Thread-1-->9
Thread-2-->9
Thread-3-->9
Thread-0-->9
Thread-4-->9
Thread-3-->8
Thread-2-->8
Thread-1-->8
Thread-0-->8
Thread-4-->8
Thread-1-->7
Thread-0-->7
Thread-3-->7
Thread-2-->7
Thread-4-->7
Thread-1-->6
Thread-2-->6
Thread-0-->6
Thread-3-->6
Thread-4-->6
Thread-1-->5
Thread-3-->5
Thread-0-->5
Thread-2-->5
Thread-4-->5
Thread-1-->4
Thread-0-->4
Thread-2-->4
Thread-3-->4
Thread-4-->4
Thread-1-->3
Thread-2-->3
Thread-0-->3
Thread-3-->3
Thread-4-->3
Thread-1-->2
Thread-0-->2
Thread-2-->2
Thread-3-->2
Thread-4-->2
Thread-1-->1
Thread-0-->1
Thread-2-->1
Thread-3-->1
Thread-4-->1
这种情况其实更好理解,因为五个线程所执行的是不同的任务,所以就不存在共享资源的问题。因为这五个线程每次所执行的是五个不同的MyTask10对象的method方法,每线程各自完成各自的任务,互不影响,
如果从对象锁的角度来说是因为五个对象都有各自的对象锁,一个线程占用一个对象的对象锁也不会影响另外一个线程去使用另一个对象的对象锁。即便没有synchrnoized来实现同步也不会出现一些错误的结果(就是打印出0,-1,-2的情况),没有资源共享时也没有必要同步。
同步静态方法:
这里我将method方法改为了static方法,static方法又称类方法,static修饰的方法或者变量在内存中只有一个副本,不管创建多少个类的实例,都只有一个副本。
package com.lql.thread;
public class MyTask10 implements Runnable {
private static int n = 10;
public MyTask10() {
}
public static synchronized void method(){
while (n > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "-->"
+ n--);
}
}
@Override
public void run() {
// TODO Auto-generated method stub
method();
}
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new MyTask10());
thread.start();
}
}
}
运行结果:
Thread-0-->10
Thread-0-->9
Thread-0-->8
Thread-0-->7
Thread-0-->6
Thread-0-->5
Thread-0-->4
Thread-0-->3
Thread-0-->2
Thread-0-->1
上边说的都是同步方法,java还提供了另外一种细粒度的实现线程同步的机制——同步代码块。
package com.lql.thread;
public class MyTask11 implements Runnable {
private int n = 10;
private String name = "";
public MyTask11(){
}
public void method(){
System.out.println(Thread.currentThread().getName() +"hello");
synchronized (this) {
while(n > 0){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +"-->" + n--);
}
}
}
@Override
public void run() {
// TODO Auto-generated method stub
method();
}
public static void main(String[] args) {
MyTask11 task11 = new MyTask11();
Thread thread = new Thread(task11);
Thread thread2 = new Thread(task11);
thread.start();
thread2.start();
}
}
Thread-0hello
Thread-1hello
Thread-0-->10
Thread-0-->9
Thread-0-->8
Thread-0-->7
Thread-0-->6
Thread-0-->5
Thread-0-->4
Thread-0-->3
Thread-0-->2
Thread-0-->1
同步方法所能实现的同步,同步代码块也都能实现。同步方法有时同步的范围太大,有时只是一小部分的代码是临界区,如果将整个方法都整成同步方法并不好,这与并发的理念是相悖的。所以就有了同步代码块。
synchrnized(this)括号中的对象指定当前线程获取哪个对象的对象锁,因为对象的锁是唯一的,所以一个线程获取了对象锁,在该线程释放锁之前,其他线程也就无法再取得该对象锁,在一个线程执行完同步块代码后,才释放该对象锁。synchrnized()括号里可以使任何一个对象,因为一个确定的对象他的对象锁是唯一的。只要一个线程取得了对象锁,其他线程都无法进入同步块。
总结以上所有内容:
1.当并发编程共享资源时,要使用同步来确保同一时刻只有一个线程进入临界区。
2.java中每个对象都有一个对象锁,synchronized就是基于对象锁来实现同步,一个线程占用了对象锁其他线程就必须等待上一个线程释放了对象锁才能进入被同步的临界区。
3.同步静态方法时,线程获取的对象锁是类对象(MyTask.class)的对象锁。无论定义多少个对象,被同步的静态方法都无动于衷。哪个线程有了类对象的对象,就让哪个线程进到同步区。
4.同步块是一种细粒度实现同步的方法,同步块指定执行到同步块的线程获取哪个对象的对象锁。由于一个确定对象的对象锁唯一,所以能够实现只有一个线程进入同步区代码执行。
以上是个人的理解,如有错误或不当欢迎批评指正。