这一章主要介绍线程同步的相关知识,什么是线程同步呢?我理解的线程同步是:当两个线程需要共享一个公有的资源时,我们引入的一套机制,让在同一时间点,只有一个线程在使用公有资源。
但是,我们知道每个线程的运行的时机我们是无法控制的,那么我们怎么样保证在同一时间不会出现多个线程访问同一资源呢?
如下面的例子,由于我们不知道,CPU到底先为那个线程分配时间片,所以我们无法确定那个线程先被执行,但是我们希望每个线程的执行代码都具有原子性,即在一个线程执行完成之后,再进行第二个线程的执行。
线程类代码:
public class MyTestThread extends Thread{ private int startNum ; public int getStartNum() { return startNum; } public void setStartNum(int startNum) { this.startNum = startNum; } @Override public void run() { //线程只有一个功能就是打印startNum后面的5个数据 for(int i = startNum ; i < startNum + 5 ; i++){ System.out.println("nowNum is -----> " + i); } } } 主函数代码: public class Main { public static void main(String [] arg){ MyTestThread newThread1 = new MyTestThread(); MyTestThread newThread2 = new MyTestThread(); newThread1.setStartNum(0); newThread2.setStartNum(5); newThread1.start(); newThread2.start(); } } 运行结果为: nowNum is -----> 0 nowNum is -----> 1 nowNum is -----> 5 nowNum is -----> 6 nowNum is -----> 7 nowNum is -----> 8 nowNum is -----> 9 nowNum is -----> 2 nowNum is -----> 3 nowNum is -----> 4
从运行结果我们可以知道,打印的数据出现了交叉的现象,在一个线程执行的过程中,另外一个线程也有机会进行打印,而我们期望的是两个线程中的一个完成了打印,另外一个线程再继续打印,那么怎么样避免这种情况的产生呢,java给我们提供了一个关键字synchronized,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码,其实每个java对象都有一个锁,而使用synchronized,能够保证该锁对象在某一时间只能分配给一个特定的线程,其他线程只能等待。
对于关键字synchronized需要注意的有以下几点:
1.synchronized是利用封锁相应的资源的方式来实现共享资源的互斥访问;
2.我们在使用synchronized的时候,需要知道我们封锁的是什么,他的作用范围是多大,针对的是该类的一个特定的对象,还是该类的所有对象,如下面的例子:
public class MyTestThread extends Thread{ private int startNum ; public int getStartNum() { return startNum; } public void setStartNum(int startNum) { this.startNum = startNum; } @Override public synchronized void run() { //线程只有一个功能就是打印startNum后面的5个数据 for(int i = startNum ; i < startNum + 5 ; i++){ System.out.println("nowNum is -----> " + i); } } } public class Main { public static void main(String [] arg){ MyTestThread newThread1 = new MyTestThread(); MyTestThread newThread2 = new MyTestThread(); newThread1.setStartNum(0); newThread2.setStartNum(5); newThread1.start(); newThread2.start(); } } 运行结果为: nowNum is -----> 0 nowNum is -----> 5 nowNum is -----> 1 nowNum is -----> 6 nowNum is -----> 2 nowNum is -----> 7 nowNum is -----> 3 nowNum is -----> 8 nowNum is -----> 4 nowNum is -----> 9
这并不是我们想要的结果,我们想要的是一个线程打印完,另外的线程再开始打印,这是为什么呢?并不是随随便便加上一个synchronized关键字就能够实现线程的同步,这是因为当前的synchronized方法加在了run方法的前面,封锁住的是当前的对象,在main函数中我们生成了两个线程对象,这样每个线程在执行run方法时都持有了它自己的对象锁,没有其他线程和它竞争,当然不能通过这种方式来实现线程的同步。
我们改变一下线程类的实现方式:
我们改变一下线程类的实现方式:
public class MyTestThread extends Thread{ private int startNum ; private static Object lockObject = new Object(); public int getStartNum() { return startNum; } public void setStartNum(int startNum) { this.startNum = startNum; } @Override public void run() { synchronized (lockObject) { //线程只有一个功能就是打印startNum后面的5个数据 for(int i = startNum ; i < startNum + 5 ; i++){ System.out.println("nowNum is -----> " + i); } } } } 这时的结果就是我们所需要的了: nowNum is -----> 0 nowNum is -----> 1 nowNum is -----> 2 nowNum is -----> 3 nowNum is -----> 4 nowNum is -----> 5 nowNum is -----> 6 nowNum is -----> 7 nowNum is -----> 8 nowNum is -----> 9
当然在这里也可能出现两种结果,一种如上图从0打印到9,另外一种是从5打印到9,再从0打印到4,这是因为,我们不知道CPU到底先调用哪个线程,有可能线程1开始执行,也有可能是线程2开始执行,但是由于我们添加的synchronized同步块,并且针对的是类的所有对象,所以当一个线程对象获得了锁后,其他的线程就必须等待它执行完同步代码块,释放锁之后才能获得对应的锁并开始执行。
当然我们也可以采用下面的方法:
采用的上面的方法同样可以实现线程的同步,通过上面两个例子我们可以知道,线程同步的关键是,多个线程必须在竞争同一个共享资源,无论是生成一个对象,然后传递给每个线程,并且每个线程都对该对象使用synchronized修饰,实现对应代码块在该对象上同步;或者是通过对类的静态变量使用synchronized进行修饰来实现线程同步,其实质都是每个线程在竞争同一个锁资源。public class MyTestThread extends Thread{ private int startNum ; private Object lockObject ; public MyTestThread(Object lockObject){ this.lockObject = lockObject; } @Override public void run() { synchronized (lockObject) { //线程只有一个功能就是打印startNum后面的5个数据 for(int i = startNum ; i < startNum + 5 ; i++){ System.out.println("nowNum is -----> " + i); } } } public int getStartNum() { return startNum; } public void setStartNum(int startNum) { this.startNum = startNum; } } public class Main { public static void main(String [] arg){ Object lockObjiect = new Object(); MyTestThread newThread1 = new MyTestThread(lockObjiect); MyTestThread newThread2 = new MyTestThread(lockObjiect); newThread1.setStartNum(0); newThread2.setStartNum(5); newThread1.start(); newThread2.start(); } }
当然,也可以把相关方法变成静态方法来实现多个线程竞争同一个锁资源(此时的锁资源是该类的锁),从而实现线程同步,因为加入静态方法是被所有类实例对象共享,因此线程对象在访问此静态方法时是互斥访问的,从而可以实现线程的同步。
3.使用synchronized保证一段代码在多线程执行时是互斥的,但是没有用synchronized修饰的代码不存在互斥的问题,在同一时间可以被多个线程访问
如下面的例子程序:
public class Command {
public synchronized void printlnNumDesc(String threadName){
for(int i=4 ; i >= 0 ; i--){
System.out.println(threadName + "---------------> " + i);
}
}
public void printlnNumAsc(String threadName){
for(int i = 5 ; i < 10 ; i++){
System.out.println(threadName + "-----------> " + i);
}
}
}
public class Main {
public static void main(String [] arg){
Command command = new Command();
MyTestThreadOne newThread1 = new MyTestThreadOne(command);
MyTestThreadTwo newThread2 = new MyTestThreadTwo(command);
newThread1.start();
newThread2.start();
}
}
public class MyTestThreadOne extends Thread{
Command command ;
public MyTestThreadOne(Command command){
this.command = command;
}
@Override
public void run() {
command.printlnNumDesc("Thread One");
}
}
public class MyTestThreadTwo extends Thread{
Command command ;
public MyTestThreadTwo(Command command){
this.command = command;
}
@Override
public void run() {
command.printlnNumAsc("Thread Two");
}
}
运行结果为:
Thread One---------------> 4
Thread One---------------> 3
Thread One---------------> 2
Thread Two-----------> 5
Thread Two-----------> 6
Thread Two-----------> 7
Thread Two-----------> 8
Thread Two-----------> 9
Thread One---------------> 1
Thread One---------------> 0
可以看出在线程1运行的过程中,线程2也可以执行相关的函数。
4. 每个对象都有一个标志锁,当一个线程访问了对象的某个synchronized数据(包括函数)时,这个对象就将被“上锁”,所有使用synchronized修饰的同一对象的代码块,函数都被锁住了,只有等待该线程执行完同步代码块,释放锁资源,其他线程才能重新获得锁,进行执行。如上例中,如果我们将Command类变成这样:
public class Command {
public synchronized void printlnNumDesc(String threadName){
for(int i=4 ; i >= 0 ; i--){
System.out.println(threadName + "---------------> " + i);
}
}
public synchronized void printlnNumAsc(String threadName){
for(int i = 5 ; i < 10 ; i++){
System.out.println(threadName + "-----------> " + i);
}
}
}
public class Command {
public void printlnNumDesc(String threadName) {
synchronized (this) {
for (int i = 4; i >= 0; i--) {
System.out.println(threadName + "---------------> " + i);
}
}
}
public void printlnNumAsc(String threadName) {
synchronized (this) {
for (int i = 5; i < 10; i++) {
System.out.println(threadName + "-----------> " + i);
}
}
}
}
上面两种实现方式是一样的效果
Thread One---------------> 4
Thread One---------------> 3
Thread One---------------> 2
Thread One---------------> 1
Thread One---------------> 0
Thread Two-----------> 5
Thread Two-----------> 6
Thread Two-----------> 7
Thread Two-----------> 8
Thread Two-----------> 9
Thread One---------------> 3
Thread One---------------> 2
Thread One---------------> 1
Thread One---------------> 0
Thread Two-----------> 5
Thread Two-----------> 6
Thread Two-----------> 7
Thread Two-----------> 8
Thread Two-----------> 9
可以看到这两个线程实现了同步,
5.对于一个类也有一个锁,所以对一个类的静态变量使用synchronized,可以将整个类的对象都锁定。
所以我们在使用线程同步时要弄清楚,加synchronized修饰的对象对应的锁的范围。
如以下两个例子:
public class Command {
private Object lockObject = new Object();
private Object lockObject2 = new Object();
private Integer lockNumber = 0 ;
private Integer lockNumber2 = 0 ;
public void printlnNumDesc(String threadName) {
synchronized(lockNumber){
for (int i = 4; i >= 0; i--) {
System.out.println(threadName + "---------------> " + i);
}
}
}
public void printlnNumAsc(String threadName) {
synchronized(lockNumber2){
for (int i = 5; i < 10; i++) {
System.out.println(threadName + "-----------> " + i);
}
}
}
}
public class Command {
private Object lockObject = new Object();
private Object lockObject2 = new Object();
private Integer lockNumber = 0 ;
private Integer lockNumber2 = 0 ;
public void printlnNumDesc(String threadName) {
synchronized(lockObject){
for (int i = 4; i >= 0; i--) {
System.out.println(threadName + "---------------> " + i);
}
}
}
public void printlnNumAsc(String threadName) {
synchronized(lockObject2){
for (int i = 5; i < 10; i++) {
System.out.println(threadName + "-----------> " + i);
}
}
}
}
他们的运行结果是不一样的。对一个不是static的对象实现synchronized,只能在同一对象的锁下实现同步,不同的对象在多个线程下依然可以同时访问。
6.因为进行同步会使其他线程,不能访问同步块,必须等待锁对象释放出来才能,所以使用同步会牺牲掉一定的性能的,所以我们必须在实际应用中好好的衡量两者的平衡点。通常我们不必同步类中所有的方法,只是针对那些必须要保证其原子性的方法或者代码块进行同步,以保证资源的正确性。
7.一个线程可以获得多个对象锁,所以在使用同步时要考虑死锁的出现请求,并消除死锁。
8.线程在sleep时,它所获得的所有锁对象是不会被释放的。
9.每个对象只有一个锁;当提到同步时,应该清楚在哪个对象上同步。
总之,我们在对需要同步的代码使用synchronized,必须要确认当一个线程执行到同步代码块的时候,它获得的是哪个对象的锁,这时候能够阻塞的线程,只有那些正在访问同样加了synchronized修饰的,并且针对的是同一锁对象,我们在使用同步时,一定要弄清楚,在那个对象上同步,锁的范围是多大,是否会引起死锁。下章将继续研究java的线程同步。