多线程语法

多线程语法

零:前言

声明:部分观点仅由思考所得,欢迎讨论和指正.

前文提到了多线程的本质“解决多线程相关的问题,就是解决除cpu外的资源的使用顺序问题”,而在保证这些资源的访问顺序上,有很多的相关语法,本文会一一解释这些语法,并用示例验证,本文整体目录如下(新版的blog页面风格,无法自动生成目录,只好单独拿出来):

一.同步

  1.1 为什么叫同步(synchronized)?为什么使用同步?

  1.2 如何使用同步?

    1.2.1 synchronized修饰 对非静态成员变量的操作

    1.2.2 synchronized修饰 非静态方法

    1.2.3 synchronized修饰 对静态成员变量的操作

    1.2.4 synchronized修饰 静态方法

    1.2.5 synchronized通过第三方对象来同步

    1.2.6 同步(synchronized)的总结

  1.3 同步的原理

二.线程间通信方法

  2.1 线程间为什么要通信?

  2.2 线程间通信方法

    2.2.1 共享内存+wait/notify

    2.2.2 lock+await/signal

    2.2.3 阻塞队列

    2.2.4 管道

三.死锁

  3.1 为什么会产生死锁

  3.2 如何避免死锁

    3.2.1 以相同的顺序请求锁

    3.2.2 超时机制

    3.2.3 死锁检测机制

四.饥饿与公平

  4.1 基本概念

  4.2 为什么会导致饥饿

  4.3 如何避免饥饿,实现公平

五.锁(Lock)

  5.1 Lock

  5.2 ReadWriteLock

  5.3 synchronized和lock区别

    5.3.1 语法上的区别

    5.3.2 特性上的区别 

  5.4 锁相关的专有名词

    5.4.1 可重入性

    5.4.2 可中断性

    5.4.3 公平性

六.关于公平的问题和本篇总结

一.同步

1.1 为什么叫同步(synchronized)?什么使用同步?

可能是借鉴了函数调用中的“同步”概念,既一个函数调用另一个函数,等待这个函数返回值后再继续进行,这种情形和一个线程等待另一个线程很类似,所以用synchronized。也有点像Ajax中设置同步,等待后台返回结果后再继续往下执行。

那为什么使用同步,一个线程在操作一个资源的时候(写操作)可能会改变这个资源的状态,如果想确保资源的状态的确定性,就不能出现多个线程同时操作这个资源(写操作)。所以需要把资源保护(synchronized)起来,一个线程等待另一个线程执行完毕之后再操作这个资源,这里的线程包括生产者和消费者,也就是说生产者和消费者也是无法操作同一块资源。

1.2 如何使用同步

1.2.1 synchronized修饰 对非静态成员变量的操作,这个时候synchronized的参数最好是是实例化后的对象(示例后解释原因)。示例如下:

package myNIO.Channel;

public class MulThread {
	public int a = 0;
	public void add(){
		synchronized(this){
			for (int i = 0; i < 50; i++) {
				a++;
				System.out.println("a:"+a);
			}
		}
	}
	public static void main(String[] args) {
		final MulThread mulThread = new MulThread();
		Thread thread1 = new Thread(new Runnable() {
			public void run() {
				mulThread.add();
			}
		});
		Thread thread2 = new Thread(new Runnable() {
			public void run() {
				mulThread.add();
			}
		});
		thread1.start();
		thread2.start();
	}
}

原因:这里参数使用this,也就是实例化后的对象。因为synchronized保护的资源是非静态变量a,在每个实例化的对象中都有一份,所以锁住的对象应该是每一个实例对象。

1.2.2 synchronized修饰非静态方法,默认的参数是this; 示例如下:

package myNIO.Channel;

public class MulThread {
	public int a = 0;
	public synchronized void add(){
		for (int i = 0; i < 50; i++) {
			a++;
			System.out.println("a:"+a);
		}
	}
	public static void main(String[] args) {
		final MulThread mulThread = new MulThread();
		Thread thread1 = new Thread(new Runnable() {
			public void run() {
				mulThread.add();
			}
		});
		Thread thread2 = new Thread(new Runnable() {
			public void run() {
				mulThread.add();
			}
		});
		thread1.start();
		thread2.start();
	}
}
原因同1.2.1

1.2.3 synchronized修饰 对静态成员变量的操作,这个时候synchronized的参数最好是类。示例如下:

package myNIO.Channel;

public class MulThread {
	public static int a = 0;
	public void add(){
//		synchronized(this){
		synchronized(MulThread.class){
			for (int i = 0; i < 50; i++) {
				a++;
				System.out.println("a:"+a);
			}
		}
	}
	public static void main(String[] args) {
		final MulThread mulThread1 = new MulThread();
		final MulThread mulThread2 = new MulThread();
		Thread thread1 = new Thread(new Runnable() {
			public void run() {
				mulThread1.add();
			}
		});
		Thread thread2 = new Thread(new Runnable() {
			public void run() {
				mulThread2.add();
			}
		});
		thread1.start();
		thread2.start();
	}
}

原因:由于是对静态变量进行操作,所以同步的参数应该是类,这样的话同一时刻只有一个线程在操作类中的这个静态变量;如果是this,那么当实例化多个对象时候,两个线程可通过两个对象来同时操作这个静态变量,这个显然是不对的。

1.2.4 synchronized 修饰静态方法

package myNIO.Channel;

public class MulThread {
	public static int a = 0;
	public synchronized static void add(){
		for (int i = 0; i < 50; i++) {
			a++;
			System.out.println("a:"+a);
		}
	}
	public static void main(String[] args) {
		final MulThread mulThread1 = new MulThread();
		final MulThread mulThread2 = new MulThread();
		Thread thread1 = new Thread(new Runnable() {
			public void run() {
				mulThread1.add();
			}
		});
		Thread thread2 = new Thread(new Runnable() {
			public void run() {
				mulThread2.add();
			}
		});
		thread1.start();
		thread2.start();
	}
}

原因,同1.2.3

1.2.5 synchronized可以通过第三方对象来同步,为什么?

因为当操作两个变量的时候,一个变量的操作不应该影响另一个变量,所以同步的参数不应该一样,示例如下:

package myNIO.Channel;

public class MulThread {
	public int a = 0;
	public int b = 0;
	Object object1 = new Object();
	Object object2 = new Object();
	public void addA(){
		synchronized (object1) {
			for (int i = 0; i < 50; i++) {
				a++;
				System.out.println("a:"+a);
			}
		}
	}
	public void addB(){
		synchronized (object2) {
			for (int i = 0; i < 50; i++) {
				b++;
				System.out.println("b:"+b);
			}
		}
	}
	public static void main(String[] args) {
		final MulThread mulThread = new MulThread();
		Thread thread1 = new Thread(new Runnable() {
			public void run() {
				mulThread.addA();
			}
		});
		Thread thread2 = new Thread(new Runnable() {
			public void run() {
				mulThread.addB();
			}
		});
		thread1.start();
		thread2.start();
	}
}

1.2.6 同步(synchronized)的总结

同步的参数其实是由同步的内容所决定的,前面的举例已经说明了。但是有个题外话,不管是再好的机制如果没有从本质上理解它,没有严谨的态度,依然会出错。下面看一个例子,一个类作为参数,和它实例化后的对象作为参数,其实不是一样东西,而是两个锁。

package myNIO.Channel;

public class MulThread {
	public static int a = 0;
	public void add1(){
		synchronized (MulThread.class) {
			for (int i = 0; i < 50; i++) {
				a++;
				System.out.println("a:"+a);
			}
		}
	}
	public void add2(){
		synchronized (this) {
			for (int i = 0; i < 50; i++) {
				a++;
				System.out.println("a:"+a);
			}
		}
	}
	public static void main(String[] args) {
		final MulThread mulThread = new MulThread();
		Thread thread1 = new Thread(new Runnable() {
			public void run() {
				mulThread.add1();
			}
		});
		Thread thread2 = new Thread(new Runnable() {
			public void run() {
				mulThread.add2();
			}
		});
		thread1.start();
		thread2.start();
	}
}

1.3 同步的原理

java虚拟机为每一个对象和类都关联一个锁。代表任何时候只允许一个线程拥有的特权。但是如果线程获取了锁,那么在它释放这个锁之前,就没有其他线程可以获取同样数据的锁了。在java程序中,只需要使用synchronized块或者synchronized方法就可以标志一个监视区域。当每次进入一个监视区域时,java 虚拟机都会自动锁上对象或者类。

二.线程间通信的方法  

2.1 线程间为什么要通信?

这里的“线程间”指的是生产者和消费者之间,一般生产者和消费者都是一个线程,生产者和消费者需要通知彼此生产和消费的状态,在产品满的时候,生产者可以休息或者做其他事情,在没有产品的时候,消费者可以休息或者做其他事情,所以需要线程间的通信,这就是线程间的通信的意义和定义。通过演示用法来循序渐进的理解。

2.2 线程间的通信方法,了解的有以下四种,下面分别用示例介绍:

2.2.1 共享内存+wait/notify

package Channel1;

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

public class MultiThread {
	/*false代表开始的时候产品为空*/
	public volatile boolean sig = false;
	public List<String> list = new ArrayList<String>(100);
	int a = 0;
	int b = 0;
	public synchronized void put(String n){
		/*如果有产品,生产者等待*/
		if(sig){
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		this.list.add(n);
		System.out.println("生产者 put() "+a++);
		/*设置产品为非空*/
		sig = true;
		notify();
	}
	public synchronized void get(){
		/*如果没有产品,消费者等待*/
		if(!sig){
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		/*如果有产品*/
		System.out.println("消费者 get() "+b++);
		/*消费完成后把产品设置为空*/
		sig = false;
		notify();
	}
	
	public static void main(String[] args) {
		MultiThread multiThread = new MultiThread();
		for (int i = 0; i <= 50; i++) {
			final int x = i;
			Thread putThread = new Thread(new Runnable() {
				public void run() {
					multiThread.put(String.valueOf(x));
					}
			});
			putThread.start();
			
			Thread getThread = new Thread(new Runnable() {
				public void run() {
					multiThread.get();
					}
			});
			getThread.start();
		}
		
	}
	
}

解释一下,在这种线程间通信方式中用到了,synchronized和sig和wait/notify;举个例子,生产者和消费者排队进入一个房间,生产者会放入一个蛋糕,而消费者会吃掉蛋糕,由于进入房间的通道很窄,一次只能有一个人通过,其他人休息;每次生产者或者消费者出来后都会喊一声叫醒所有人,并且在房间上挂一个牌子,如果是绿色,说明下一个生产者可以进入,如果是红色,说明下一个消费者可以进入。在这个例子中,生产者和消费者指两个线程,通道代表synchronized,蛋糕代表list被赋新值,wait代表休息,notify代表侥幸,牌子代表sig这个信号。这个模型是的list中只能存一个数据。

2.2.2 lock与await/signal

这种方式和上面很相似,不举示例了。

2.2.3 blockingqueue 阻塞队列

其实阻塞队列完成了上面的synchronized代码块(用lock代替),sig信号的改变(用原子操作代替,如getAndIncrement方法),wait和notify(用await和signal代替)等操作,其实就是把相关的操作都放入阻塞队列中方法的实现上了。所以消费者模型现在只需要使用一个阻塞队列,然后直接使用其中的方法即可,如下:

package Channel1;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class MultiThread2 {
	BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<String>(1);
	
	public static void main(String[] args) {
		MultiThread2 multiThread = new MultiThread2();
		
		Thread putThread = new Thread(new Runnable(){
			public void run() {
				for (int i = 1; i <= 50; i++) {
				try {
					multiThread.blockingQueue.put(String.valueOf(i));
					System.out.println("put:"+i);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				}
			}
		});
		putThread.start();
		try {
			Thread.currentThread().sleep(10);
		} catch (InterruptedException e1) {
			e1.printStackTrace();
		}	
		Thread takeThread = new Thread(new Runnable() {
			public void run() {
				for (int j = 1; j <=50; j++) {
					try {
						multiThread.blockingQueue.take();
						System.out.println("get:"+j);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		});
		takeThread.start();
	}
}

阻塞队列的实现主要有以下几种:

ArrayBlockingQueue
LinkedBlockingQueue
DelayQueue
PriorityBlockingQueue
SynchronousQueue

这几种队列都可以设置它的公平性(关于饥饿和公平的概念随后讲)

2.2.4 管道

适用于两个线程间的通信,前面文章总结过:http://blog.csdn.net/Jintao_Ma/article/details/53103747

三. 死锁

3.1 为什么会产生死锁?

当两个线程以不同的顺序请求一组锁时,就会产生死锁,举个例子;

package Channel1;

public class MultiThread3 {
	Object objectA = new Object();
	Object objectB = new Object();
	public void methodA(){
		synchronized (objectA) {
			try {
				Thread.currentThread().sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized(objectB){
			}
		}
	}
	public void methodB(){
		synchronized (objectB) {
			try {
				Thread.currentThread().sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			synchronized (objectA) {
			}
		}
	}
	public static void main(String[] args) {
		MultiThread3 multiThread3 = new MultiThread3();
		Thread thread1 = new Thread(new Runnable() {
			public void run() {
				multiThread3.methodA();
				System.out.println("1");
			}
		});
		Thread thread2 = new Thread(new Runnable() {
			public void run() {
				multiThread3.methodB();
				System.out.println("2");
			}
		});
		thread1.start();
		thread2.start();
	}	
	
}

运行后,无法打印1,2,证明了死锁的发生

3.2 如何避免死锁

3.2.1 按照相同的顺序请求锁 如果上面的例子中,methodA和methodB以相同的顺序请求锁,就不会产生上面的死锁问题了。

3.2.2 超时机制 即超过一定的时间无法获得锁就释放锁,synchronized无法设置超时时间,需要使用JDK以后的lock中的方法,如tryLock,这个先举例,随后会讲到:

Lock lock = ...;
if(lock.tryLock(long time, TimeUnit unit)) {
     try{
         //处理任务
     }catch(Exception ex){
         
     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}

3.2.3 死锁检测机制

检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)

四.饥饿与公平

4.1 基本概念

如果一个线程因为CPU时间全部被其他线程抢走而得不到CPU运行时间,这种状态被称之为“饥饿”。解决饥饿的方案被称之为“公平性” – 即所有线程均能公平地获得运行机会。

4.2 java中导致饥饿的原因

1)高优先级线程吞噬所有的低优先级线程的CPU时间。
2)线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
3)线程在等待一个本身(在其上调用wait())也处于永久等待完成的对象,因为其他线程总是被持续地获得唤醒。

4.3 如何实现公平

使用公平锁。前面讲到阻塞队列的问题,是使用lock来实现公平,即放入请求队列的顺序,就是请求锁的顺序。'

五.锁

锁是jdk1.5之后才有的语法,实现了比synchronized更多的功能

5.1 Lock的语法

Lock是一个接口,ReentrantLock(意思是可重入锁,后面介绍)是唯一实现了Lock接口的类,关于ReentrantLock的使用有四种常用的方法:

5.1.1 lock()方法

首先lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。它的使用形式如下:

package Channel1;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MultiThread4 {
	Lock lock = new ReentrantLock();
	public int a = 0;
	public void add(){
		lock.lock();
		try {
			for (int i = 0; i < 50; i++) {
				a++;
				System.out.println("a"+a);
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}
	
	public static void main(String[] args) {
		MultiThread4 multiThread4 = new MultiThread4();
		Thread thread1 = new Thread(new Runnable() {
			public void run() {
				multiThread4.add();
			}
		});
		Thread thread2 = new Thread(new Runnable() {
			public void run() {
				multiThread4.add();
			}
		});
		thread1.start();
		thread2.start();
	}
}

如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

5.1.2 tryLock()与tryLock(long time, TimeUnit unit)

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。它的一般使用形式如下:

public void method() throws InterruptedException {
    lock.lockInterruptibly();
    try {  
     //.....
    }
    finally {
        lock.unlock();
    }  
}

5.2 ReadWriteLock

ReadWriteLock也是一个接口,如下:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading.
     */
    Lock readLock();
 
    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing.
     */
    Lock writeLock();
}

一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。下面的ReentrantReadWriteLock实现了ReadWriteLock接口。示例如下:

package Channel1;

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MultiThread5 {
	ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
	public int a = 0;
	public void add(){
		readWriteLock.readLock().lock();
//		readWriteLock.writeLock().lock();
		try {
			for (int i = 0; i < 50; i++) {
				a++;
				System.out.println("a"+a);
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			readWriteLock.readLock().unlock();
//			readWriteLock.writeLock().unlock();
		}
	}
	
	public static void main(String[] args) {
		MultiThread5 multiThread5 = new MultiThread5();
		Thread thread1 = new Thread(new Runnable() {
			public void run() {
				multiThread5.add();
			}
		});
		Thread thread2 = new Thread(new Runnable() {
			public void run() {
				multiThread5.add();
			}
		});
		thread1.start();
		thread2.start();
	}
}

当使用读锁时,可以同时操作a的值,当使用写锁的时候,则同一时刻只能有一个线程改变a的值。

5.3 synchronized和lock的区别

5.3.1 在语法(或者说使用方法)上的区别

1)Lock是一个接口,而synchronized是java中的关键字,后者是内置的语言实现
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不易导致死锁现象发生;而Lock在发生异常时,如果没有主动去释放锁,则给系统增加了死锁的可能性。

5.3.2 在特性上的区别

1)Lock可以知道有没有成功获取锁,如上面的tryLock,而synchronized无法办到。
2)Lock可以使线程响应中断,而synchronized不行。
3)Lock可以提高多个线程进行读操作的效率

5.4 锁的一些专有名词

5.4.1 可重入性

就是说线程获得锁后,在锁内的代码中,如果需要再次用到该锁,不必重新申请。这其实说明了锁的分配机制,即按照线程的分配,当线程获得锁,那么在该线程执行过程中,
所有涉及到该锁的地方都不用重新申请,这就是可重入性。synchronized和lock都具备可重入性。

5.4.2 可中断性

即使用该锁的线程可以响应中断。synchronized不具备可中断性,而Lock具备,示例如下:

package myNIO.Channel;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MultiThread2 {
	Lock lock = new ReentrantLock();
	public int a = 0;
	public void add() throws InterruptedException{
		lock.lockInterruptibly();
		try {
			for (int i = 0; i < 50; i++) {
				a++;
				System.out.println("a:"+a);
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally{
//			lock.unlock();
		}
	}
	public static void main(String[] args) throws InterruptedException {
		final MultiThread2 multiThread2 = new MultiThread2();
		Thread thread1 = new Thread(new Runnable() {
			public void run() {
				try {
					multiThread2.add();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		});
		Thread thread2 = new Thread(new Runnable() {
			public void run() {
				try {
					multiThread2.add();
				} catch (InterruptedException e) {
					System.out.println("MultiThread2.main() thread2被中断");
				}
			}
		});
		thread1.start();
		thread2.start();
		Thread.currentThread().sleep(100);
		thread2.interrupt();
	}
}

其中Thread1一直占有锁,而Thread2一直尝试获取锁,这个时候给中断Thread2,它是可以响应的,从而打印被中断的信息。

5.4.3 公平性

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。
synchronized是非公平锁。而Lock是公平锁,如ReentrantLock和ReentrantReadWriteLock可以通过参数设置公平性,可以是公平或者非公平的;


6.关于公平的问题

前面说到了公平队列,就是使用Lock来实现公平。而Lock中的公平性,即按照请求锁的顺序来使用锁。 以线程池为例,所有的请求任务被按请求锁顺序放入队列中,如put方法,所有的响应线程按照请求锁的顺序来处理队列中的任务,如take方法。只需要知道在从任务队列里拿任务时是公平的即可,至于线程池中的线程的产生和使用是不是公平的(即线程池是否存在一个线程始终得不到执行),我们并不关心,为什么?  因为不管线程池的采用什么数据结构实现或者有没有线程一直得不到执行,所有线程执行的任务是一样的,没有特殊性,即使存在一个线程不执行,也可以由于其他线程来执行;并且,线程池会回收销毁一直不执行的线程,那么这个问题就更不是问题了,综上所述,从任务的角度来说,所有的任务都被执行了,从内存和cpu调度角度,没有线程在浪费内存和cpu。

本篇总结:多线程的语法暂时总结这些,有时间再深究细节问题。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值