如果你想要一个程序运行的更快,那么可以将其断开为多个片段,在单独的处理器上运行每个片段。并发是用于多处理器编程的基本工具。为了使程序运行得更快,你必须学习如何利用这些额外的处理器,而则正是并发赋予你的能力。
如果你有一台多处理器的机器,那么就可以在这些处理器之间分部多个任务,从而可以极大的提高吞吐量。这是使用强有力的多处理器Web服务器的常见情况,在为每一个请求分配一个线程的程序中,它可以将大量的用户请求分布到多个CPU上。
但是,并发通常是提高运行在单处理器上的程序的性能。
因为,在单处理器上运行的并发程序开销应该比改程序的所有部分都顺序执行的开销大,因为其中增加了所谓的上下文切换
的代价(从一个任务切换到另一个任务)。表面上看,将程序的所有部分当作单个的任务运行好像是开销更小一点,并且可以节省上下文切换的代价。
使得这个问题变得有些不同的是阻塞
。如果程序中的某个任务因为该程序控制范围之外的某系条件(通常是I/O)而导致不能继续执行,那么我们就说这个任务或现场阻塞
了。如果没有并发,则整个程序都将停止下来,直到外部条件发生变化。但是,如果使用并发来编写程序,那么当一个任务阻塞时,程序中的其他任务还可以继续执行,因此这个程序可以保持继续向前执行。事实上,从性能的角度看,如果没有任务阻塞,那么在单处理器上使用并发就没有任何意义。
在单处理器系统中的性能提高的常见实例是事件驱动的编程
。实际上,使用并发最吸引人的一个原因就是要产生具有可响应的用户界面。这也是在Android系统中不允许子线程总访问UI的原因,在Android中的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态。那为什么系统不对UI控件的访问加上锁机制呢?原因有两个:其一,加上锁会让UI访问的逻辑变的复杂;其二,锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。所以在Android系统中最简单而且高效的方法就是采用单线程模型来处理UI操作。
实现并发最直接的方式是在操作系统级别使用进程
。进程是运行在它自己的地址空间内的自包容的程序。多任务操作系统可以通过周期性地将CPU从一个进程切换到另一个进程,来实现同时运行多个(进程)程序,尽管这使得每个进程看起来在其执行过程中都是歇歇停停。进程被操作系统互相隔开,因此不会彼此干涉,这使得用进程编程相对容易一些。与此相反,像Java所使用的这种并发系统会共享诸如内存和I/O这样的资源,因此编程编写多线程程序最基本的困难在于,协调不同线程驱动的任务之间对这些资源的使用,以便使得这些资源不会同时被多个任务访问。
线程包括5种状态:
1、新建(New):线程对象被创建时,它只会短暂地处于这种状态。此时它已经分配了必须的系统资源,并执行了初始化。例如,Thread thread = new Thread()。
2、就绪(Runnable):称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
3、运行(Running):线程获取CPU权限进行执行。注意:线程只能从就绪状态进入运行状态。
4、阻塞(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分为三种:
(1)等待阻塞:通过调用线程的wait()方法,让线程等待某工作的完成。
(2)同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态。
(3)其他阻塞:通过调用线程的sleep()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或是超时。或是I/O处理完毕时,线程重新转入就绪状态。
5.死亡(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
这5中状态涉及到的内容包括Object类,Thread类和synchronized关键字。
在Object类中,定义了wait(),notify(),notifyAll()等一系列休眠/唤醒函数。
在Thread类中,定义了一系列线程操作函数,例如,sleep()休眠函数,interrupt中断()函数,getName()获取线程名称等。
synchronized关键字,它是区分synchronized代码块和synchronized方法。synchronized的作用是让线程获取对象的同步锁。
常用的实现多线程的两种方式:Thread和Runnable。之所以说是“常用”,是因为在Java 5后可以通过java.util.concurrent包中的线程池来实现多线程。关于多线池的内容我会在java多线程之进阶篇
中介绍。Runnable是一个接口,该接口包含了一个run()
方法,Runnable具有更好的扩展性。Thread是一个类,Thread本身就是实现了Runnable接口。此外,Runnable还可以用于“资源共享”。即,多个线程都是基于某个Runnable对象建立的,它们会共享Runnable对象上的资源
。
线程可以驱动任务,因此你需要一种描述任务的方式,在Java中用Runnable
接口来提供。需要定义任务,只需要实现Runnable接口并编写run()
方法,使得该任务可以执行你的命令。
public class RunnableTest {
/**
* @param args
*/
public static void main(String[] args) {
class MyRunnable implements Runnable{
private int ticket=10;
@Override
public void run() {
for(int i=0;i<20;i++){
if(this.ticket>0){
System.out.println(Thread.currentThread().getName()+" 卖票:ticket "+this.ticket--);
}
}
}
}
// 启动3个线程t1,t2,t3(它们共用一个Runnable对象),这3个线程一共卖10张票!这说明它们是共享了MyRunnable接口的。
MyRunnable runnable = new MyRunnable();
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
Thread t3 = new Thread(runnable);
t1.start();
t2.start();
t3.start();
}
}
运行结果:
Thread-0 卖票:ticket 10
Thread-2 卖票:ticket 8
Thread-1 卖票:ticket 9
Thread-2 卖票:ticket 6
Thread-0 卖票:ticket 7
Thread-0 卖票:ticket 3
Thread-0 卖票:ticket 2
Thread-0 卖票:ticket 1
Thread-2 卖票:ticket 4
Thread-1 卖票:ticket 5
将Runnable对象转变为工作任务的传统方式是把它提交给一个Thread构造器。
public class ThreadTest {
/**
* @param args
*/
public static void main(String[] args) {
class MyThread extends Thread{
private int ticket = 10;
public void run(){
for(int i=0;i<20;i++){
if(this.ticket>0){
System.out.println(this.getName()+"买票:ticket "+this.ticket--);
}
}
}
}
// 启动3个线程t1,t2,t3;每个线程各卖10张票!
// 和上面的结果对比,并揣摩 “Runnable还可以用于“资源共享”。即,多个线程都是基于某个Runnable对象建立的,它们会共享Runnable对象上的资源”这句话。
MyThread t1 =new MyThread();
MyThread t2 =new MyThread();
MyThread t3 =new MyThread();
t1.start();
t2.start();
t3.start();
}
}
运行结果:
Thread-0买票:ticket 10
Thread-2买票:ticket 10
Thread-1买票:ticket 10
Thread-2买票:ticket 9
Thread-0买票:ticket 9
Thread-2买票:ticket 8
Thread-1买票:ticket 9
Thread-2买票:ticket 7
Thread-0买票:ticket 8
Thread-2买票:ticket 6
Thread-1买票:ticket 8
Thread-1买票:ticket 7
Thread-2买票:ticket 5
Thread-0买票:ticket 7
Thread-2买票:ticket 4
Thread-1买票:ticket 6
Thread-2买票:ticket 3
Thread-0买票:ticket 6
Thread-2买票:ticket 2
Thread-1买票:ticket 5
Thread-2买票:ticket 1
Thread-0买票:ticket 5
Thread-1买票:ticket 4
Thread-0买票:ticket 4
Thread-1买票:ticket 3
Thread-0买票:ticket 3
Thread-1买票:ticket 2
Thread-0买票:ticket 2
Thread-1买票:ticket 1
Thread-0买票:ticket 1
synchronized关键字是为了解决共享资源竞争的问题,共享资源一般是以对象形式存在的内存片段,但也可以是文件、输入/输出端口,或者是打印机。要控制对共享资源的访问,得先把它包装进一个对象。然后把所有要访问的这个资源的方法标记为synchronized。如果某个任务处于一个对标记为synchronized的方法的调用中,那么在这个线程从该方法返回之前,其他所有要调用类中任何标记为synchronized方法的线程都会被阻塞。所有对象都自动含有单一的锁(也称为监视器)。当在对象上调用其任意synchronized方法的时候,对象都被加锁,这时该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。
在Java中,每个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在的。当我们调用某个对象的synchronized方法时,就获得了该对象的同步锁,不同线程对同步锁的访问是互斥的。因为锁语句产生了一种互相排斥的效果,所以这种机制常常称为互斥量(mutex)
。
我们将synchronized的基本规则总结为下面3条,并通过实例对它们进行说明。
第一条
:当一个线程访问某对象
的synchronized方法
或者synchronized代码块
时,其他线程对该对象
的该synchronized方法
或者synchronized代码块
的访问将被阻塞。
第二条
:当一个线程访问某对象
的synchronized方法
或者synchronized代码块
时,其他线程仍然可以访问该对象
的非同步代码块。
第三条
:当一个线程访问某对象
的synchronized方法
或者synchronized代码块
时,其他线程对该对象
的其他的synchronized方法
或者synchronized代码块
的访问将被阻塞。
第一条
:当一个线程访问某对象
的synchronized方法
或者synchronized代码块
时,其他线程对该对象
的该synchronized方法
或者synchronized代码块
的访问将被阻塞。
//实例1
public class RunnableTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
class MyRunnable implements Runnable{
private int j=5;
@Override
public void run() {
synchronized(this){
for(int i=0;i<5;i++){
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+" loop "+i);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
Runnable runnable = new MyRunnable();
Thread t1 = new Thread(runnable,"t1");
Thread t2 = new Thread(runnable,"t2");
t1.start();
t2.start();
}
}
运行结果:
t1 loop 0
t1 loop 1
t1 loop 2
t1 loop 3
t1 loop 4
t1 loop 5
t1 loop 6
t1 loop 7
t1 loop 8
t1 loop 9
t2 loop 0
t2 loop 1
t2 loop 2
t2 loop 3
t2 loop 4
t2 loop 5
t2 loop 6
t2 loop 7
t2 loop 8
t2 loop 9
结果说明:run()方法中存在synchronized(this)代码块,而且t1和t2都是基于MyRunnable这个Runnable对象创建的线程。这就意味着,我们可以将synchronized(this)中的this看做是MyRunnable这个Runnable对象;因此,线程t1和t2共享“MyRunable对象的同步锁”。所以,当一个线程运行的时候,另外一个线程必须等待正在运行的线程释放MyRunnable的同步锁之后才能运行。
下面一个极其相似的例子:
//实例2
public class ThreadTest {
/**
* @param args
*/
public static void main(String[] args) {
class MyThread extends Thread{
public MyThread(String name){
super(name);
}
@Override
public void run() {
synchronized(this){
for(int i=0;i<10;i++){
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+" loop "+i);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
Thread t1 = new MyThread("t1");
Thread t2 = new MyThread("t2");
t1.start();
t2.start();
}
}
运行结果:
t2 loop 0
t1 loop 0
t2 loop 1
t1 loop 1
t1 loop 2
t2 loop 2
t2 loop 3
t1 loop 3
t1 loop 4
t2 loop 4
t2 loop 5
t1 loop 5
t1 loop 6
t2 loop 6
t1 loop 7
t2 loop 7
t2 loop 8
t1 loop 8
t2 loop 9
t1 loop 9
对比结果,你是否可以正确且快速的明白呢?上面的实例1是实现了Runnable接口,实例2继承了Thread类。在run()方法中都有synchronized(this),实例1的结果是先执行线程t1然后才是线程t2,实例2的结果是线程t1和t2交替执行。
分析:synchronized(this)中的this是指当前对象
,即synchronized(this)所在类对应的当前对象
。它的作用是获取获取当前对象的同步锁
。对于实例2中的synchronized(this)中的this代表的是MyThread对象,t1和t2是两个不同的MyThread对象,因此t1和t2在执行synchronized(this)时获取的是不同对象的同步锁。对于实例1来说,synchronized(this)中的this代表的时候MyRunnable对象,t1和t2是共同一个MyRunnable对象,因此,一个线程获取了对象的同步锁,会造成另一个线程的等待。
第二条
:当一个线程访问某对象
的synchronized方法
或者synchronized代码块
时,其他线程仍然可以访问该对象
的非同步代码块。
public class SyncAndNoSync {
/**
* @param args
*/
public static void main(String[] args) {
class Count {
// 含有synchronized同步块的方法
public void synMethod() {
synchronized(this) {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // 休眠100ms
System.out.println(Thread.currentThread().getName() + " synMethod loop " + i);
}
} catch (InterruptedException ie) {
}
}
}
// 非同步的方法
public void nonSynMethod() {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " nonSynMethod loop " + i);
}
} catch (InterruptedException ie) {
}
}
}
final Count count = new Count();
// 新建t1, t1会调用“count对象”的synMethod()方法
Thread t1 = new Thread(
new Runnable() {
@Override
public void run() {
count.synMethod();
}
}, "t1");
// 新建t2, t2会调用“count对象”的nonSynMethod()方法
Thread t2 = new Thread(
new Runnable() {
@Override
public void run() {
count.nonSynMethod();
}
}, "t2");
t1.start(); // 启动t1
t2.start(); // 启动t2
}
}
运行结果:
t2 nonSynMethod loop 0
t1 synMethod loop 0
t2 nonSynMethod loop 1
t1 synMethod loop 1
t2 nonSynMethod loop 2
t1 synMethod loop 2
t1 synMethod loop 3
t2 nonSynMethod loop 3
t2 nonSynMethod loop 4
t1 synMethod loop 4
结果说明:
线程t1和t2交替执行。t1会调用count对象的synMethod()方法,该方法中含有同步块;而t2则会调用count对象的nonSynMethod()方法,该方法不是同步方法。t1运行时,虽然调用synchronized(this)获取count对象的同步锁;但是并没有造成t2的阻塞,因为t2没有用到count对象的同步锁。
第三条
:当一个线程访问某对象
的synchronized方法
或者synchronized代码块
时,其他线程对该对象
的其他的synchronized方法
或者synchronized代码块
的访问将被阻塞。
public class SyncAndSync {
/**
* @param args
*/
public static void main(String[] args) {
class Count {
// 含有synchronized同步块的方法
public void synMethod() {
synchronized(this) {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // 休眠100ms
System.out.println(Thread.currentThread().getName() + " synMethod loop " + i);
}
} catch (InterruptedException ie) {
}
}
}
// 也包含synchronized同步块的方法
public void synMethod2() {
synchronized(this) {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " synMethod2 loop " + i);
}
} catch (InterruptedException ie) {
}
}
}
}
final Count count = new Count();
// 新建t1, t1会调用“count对象”的synMethod()方法
Thread t1 = new Thread(
new Runnable() {
@Override
public void run() {
count.synMethod();
}
}, "t1");
// 新建t2, t2会调用“count对象”的synMethod2()方法
Thread t2 = new Thread(
new Runnable() {
@Override
public void run() {
count.synMethod2();
}
}, "t2");
t1.start(); // 启动t1
t2.start(); // 启动t2
}
}
运行结果:
t1 synMethod loop 0
t1 synMethod loop 1
t1 synMethod loop 2
t1 synMethod loop 3
t1 synMethod loop 4
t2 synMethod2 loop 0
t2 synMethod2 loop 1
t2 synMethod2 loop 2
t2 synMethod2 loop 3
t2 synMethod2 loop 4
结果说明:
t1和t2运行时都调用synchronized(this),这个this是Count对象(count),而t1和t2共用count。因此,在t1运行时,t2会被阻塞,等待t1运行释放“count对象的同步锁”,t2才能运行。
synchronized方法是用synchronized修饰方法,这是一种粗粒度锁;这个同步方法(非static方法)无需显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。
synchronized代码块是用synchronized修饰代码块,这是一种细粒度锁。线程开始执行同步代码块之前,必须先获得对同步监视器的锁定,任何时候只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对同步监视器的锁定。虽然Java允许使用任何对象作为同步监视器,但同步监视器的目的就是为了阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。
public class SnchronizedTest {
public static void main(String[] args) {
class Demo {
public synchronized void synMethod() {
for(int i=0; i<1000000; i++)
;
}
public void synBlock() {
synchronized( this ) {
for(int i=0; i<1000000; i++)
;
}
}
}
Demo demo = new Demo();
long start,diff;
start = System.currentTimeMillis();
demo.synMethod(); // 调用“synchronized方法块”
diff = System.currentTimeMillis() - start; // 获取“时间差值”
System.out.println("synMethod() : "+ diff);
start = System.currentTimeMillis();
demo.synBlock(); // 调用“synchronized方法块”
diff = System.currentTimeMillis() - start; // 获取“时间差值”
System.out.println("synBlock() : "+ diff);
}
}
运行结果:
synMethod() : 24
synBlock() : 9
结果说明:synchronized代码块可以更精确的控制冲突限制访问区域,有时候表现更高效率。
实例锁:锁在某个实例对象上。如果该类是单例,那么该锁也是具有全局锁的概念。实例锁对应的就是synchronized关键字。
全局锁:该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。
关于实例锁和全局锁有个很好的例子。
pulbic class Something {
public synchronized void isSyncA(){}
public synchronized void isSyncB(){}
public static synchronized void cSyncA(){}
public static synchronized void cSyncB(){}
}
假设,类Something有两个实例(对象)分别为x和y。分析下面4组表达式获取锁的情况。
(01) x.isSyncA()与x.isSyncB()
(02) x.isSyncA()与y.isSyncA()
(03) x.cSyncA()与y.cSyncB()
(04) x.isSyncA()与Something.cSyncA()
这是因为isSyncA()和isSyncB()都是访问的同一个对象(对象x)的同步锁。
public class LockTest {
static class Something{
public synchronized void isSyncA(){
for(int i=0;i<5;i++){
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+" : isSyncA");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public synchronized void isSyncB(){
for(int i=0;i<5;i++){
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName()+" : isSyncB");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public static synchronized void cSyncA(){
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // 休眠100ms
System.out.println(Thread.currentThread().getName()+" : cSyncA");
}
}catch (InterruptedException ie) {
}
}
public static synchronized void cSyncB(){
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // 休眠100ms
System.out.println(Thread.currentThread().getName()+" : cSyncB");
}
}catch (InterruptedException ie) {
}
}
}
Something x = new Something();
Something y = new Something();
private void test1(){
Thread t11 = new Thread(new Runnable(){
@Override
public void run() {
x.isSyncA();
//x.cSyncA();
}
},"t11");
Thread t12 = new Thread(new Runnable(){
@Override
public void run() {
x.isSyncB();
//y.isSyncA();
//y.cSyncB();
//Something.cSyncA();
}
},"t12");
t11.start();
t12.start();
}
public static void main(String[] args) {
LockTest lockTest1 = new LockTest();
lockTest1.test1();
}
}
运行结果:
t11 : isSyncA
t11 : isSyncA
t11 : isSyncA
t11 : isSyncA
t11 : isSyncA
t12 : isSyncB
t12 : isSyncB
t12 : isSyncB
t12 : isSyncB
t12 : isSyncB
因为访问的不是同一个对象的同步锁,x.isSyncA()访问的是x的同步锁,而y.isSyncA()访问的是y的同步锁。
代码只需要在4.4.2.1上的代码将x.isSyncA() 和y.isSyncA()的注释去掉,其他的语句加上注释。
运行结果:
t11 : isSyncA
t12 : isSyncA
t11 : isSyncA
t12 : isSyncA
t12 : isSyncA
t11 : isSyncA
t12 : isSyncA
t11 : isSyncA
t12 : isSyncA
t11 : isSyncA
因为cSyncA()和cSyncB()都是static类型,x.cSyncA()相当于Something.isSyncA(),y.cSyncB()相当于Something.isSyncB(),因此它们共用一个同步锁,不能被同时反问。
代码只需要在4.4.2.1上的代码将 x.cSyncA()与y.cSyncB() 的注释去掉,其他的语句加上注释。
运行结果:
t12 : cSyncB
t12 : cSyncB
t12 : cSyncB
t12 : cSyncB
t12 : cSyncB
t11 : cSyncA
t11 : cSyncA
t11 : cSyncA
t11 : cSyncA
t11 : cSyncA
因为isSyncA()是实例方法,x.isSyncA()使用的是对象x的锁;而cSyncA()是静态方法,Something.cSyncA()可以理解对使用的是“类的锁”。因此,它们是可以被同时访问的。
代码只需要在4.4.2.1上的代码将 x.isSyncA()与Something.cSyncA() 的注释去掉,其他的语句加上注释。
运行结果:
t12 : cSyncA
t11 : isSyncA
t11 : isSyncA
t12 : cSyncA
t11 : isSyncA
t12 : cSyncA
t11 : isSyncA
t12 : cSyncA
t11 : isSyncA
t12 : cSyncA
估计这一篇有点太长了,如果你有兴趣,可以接着看Java多线程之基础篇(二)
站在巨人的肩膀上:《Java编程思想》和 大神之作