Java多线程编程

线程是独立的线程,它代表独立的执行空间。Thread 是java.lang这个包里的一个类,因此是自动import的,Thread对象代表线程,当你需要启动新的线程时就建立Thread的实例。

每个java应用程序会启动一个主线程----将main()放在它自己执行空间的最开始处。java虚拟机会负责主线程的启动,程序员负责启动自己建立的线程。

线程要记录的一项事物是目前线程执行空间做到了哪里。

如何启动新的线程

  • 1 建立Runnale对象(线程的任务)
Runnale threadJob = new MyRunnable();

myRunnale是你编写的实现Runnable的类,注意Runnable是个接口,因此不管怎么写都应该是public的,这个类就是你对线程要执行的任务的定义。也就是说此方法会在线程的执行空间运行。必须要实现Runnable类的run()方法。

public class MyRunnable implements Runnable{

public void run(){
go();
}

public void go(){
System.out.println("top o the stack");
}

}
  • 2 建立Thread 对象并赋值Runnable(任务)
Thread myThread = new Thread(threadJob);

把Runnable对象传给Thread构造函数,这会告诉Thread对象要把那个方法放到执行空间去运行----Runnable的run()方法。

  • 3 启动Thread
myThread.start();

在还没有调用Thread的start()方法之前什么也不会发生,当新的线程启动之后,它会把Runnable对象的方法摆到心的执行空间中。

对于Thread而言,它只是一个工人,而Runnable就是这个工人的工作。

线程怎么会知道要先执行哪个方法?

因为Runnable定义了一个协约,由于Runnable是一个接口,线程的任务被定义在任何实现Runnable的类上,线程只在乎传入给Thread的构造函数的参数是否为实现Runnable的类。当你把参数传给Thread的构造函数时,实际上就是在给Thread取的run()方法。这就等于你给Thread一项任务。

线程调度器

线程调度器会决定哪个线程从等待状况中被挑出来运行,以及何时把哪个线程送回等待被执行的状态。它会决定某个线程要运行多久,当线程被踢出时,调度器会指定线程要回去等待下一个机会或者暂时地堵塞。

注意你无法控制调度,没有API可以调用调度器。它通常是公平的,但是没人能保证这件事。有时候某些线程很受宠,某些线程会被冷落。并且每个线程执行的顺序可能会不唯一,即使你在同一台机器上运行同一个程序。

典型的可执行/执行中循环

通常线程会在可执行与执行中两种状态中来回交替,因为java虚拟机的线程调度会把线程跳出来运行又把它踢回去使得其他的线程有执行机会。

线程有可能会被暂时挡住

调度器会因为某些原因把线程送进去关一阵子。例如线程本身的程序会要求小睡一下sleep(),也有可能是因为线程调用某个被锁住的对象上的方法,此时线程就得等到锁住该对象的线程放开这个对象才能继续下去。

这类型的条件都会导致线程暂时失能。

Thread对象可以重复使用吗,能否调用start()指定新的任务给它?

答案是不行,一旦线程的run()方法完成之后,该线程就不能再重新启动,事实上过了改点线程就死翘翘了。

使用sleep()让程序更加可预测

try{
Thread.sleep(2000);
}catch(Exception x){x.printStackTrace();}

如果想要确保其他的线程有机会执行的话,就把线程放入睡眠状态,当线程醒来的时候,它会进入可执行状态等待被调度器挑出来执行。注意这个方法可能会抛出InterruptedException异常,因此需要包含在try/catch模块中。

给线程取名字

通常给线程取名字是为了调试。

Runnable runner = new MyRunnable();
Thread alpha = new Thread(runner);
Thread beta = new Thread(runner);
alpha.setName("Alpha Thread");
beta.setName("Beta Thread");

String threadName = Thread.currentThread.getName();

多线程并发问题

问题的根源在于,当两个线程存取单一对象的数据时,也就是说两个不同执行空间上的方法都在堆上对同一个对象执行setter/getter,两个进程只会关心自己的任务,因为线程可能会被打入可执行状态,此时基本上是昏迷过去的,当它回到执行中的状态时,根本不知道自己曾经不省人事。此时对象的状态或许早已不是它熟睡前的状态了。

使用synchronized关键字来修饰方法使得它每次只能被单一的线程存取。要保护数据,就要把作用在数据上的方法同步化。

每个java对象都有一个锁,每个锁只有一把钥匙,通常对象都没上锁,也没有人关心这件事。但如果对象有同步化的方法,则线程只能在取的钥匙的情况下进入线程。也就是说并没有其他线程已经进入的情况下才能进入。

锁不是配在方法上,虽然synchronized修饰的是方法,而是配在对象上,如果对象有两个同步化的方法,就表示两个线程无法进入同一个方法,也表示两个线程无法进入不同的方法。

想想看,如果你有多个方法可能会操作对象的实例变量,则这些方法都应该要有同步化保护。

同步化的目标是要保护重要的数据,但是要记住,你锁住的不是数据,而是存取数据的方法。

当线程进入同步化方法时,线程会全力照顾好它的钥匙,除非完成同步化方法,否则会放开钥匙。没有其他的线程能进入该对象的同步化方法,因为每个对象只有一个钥匙。

“丢失更新”问题

当线程A执行某个方法时,中途睡着了,另外一个线程B也执行这个方法,对数据对象作出了一些更新,然后程序回到执行A,此时A将忘记自己曾经熟睡这件事,而从自己当初退出的状态开始继续执行,于是进程B所做的更新被A丢掉了。

解决的方法是,为方法加上synchronized关键字,确保其他线程可以进入该方法之前所有的步骤都会完成(如同原子不可分割一样)。

这里有时候不需要把整个方法都同步化,只需要把不可分割的步骤同步化,组成原子单位,换句话说,一个方法里不是所有的步骤都不可分割。

public void go(){
doStuff();

synchronized(this){
criticalStuff();
moreCriticalStuff();
}

}

通常会以当前对象(this)来同步化。

死锁

死锁会发生时因为两个线程互相持有对方正在等待的东西。没有方法可以脱离这种情况,所以两个线程只好停下来等,一直等。

同步化的原则

听起来把所有的东西都同步化是个不错的注意,如此一来全部都会具有多线程执行的安全性,但是当然好东西也会有坏的地方:

  • 代价

    同步化的方法有额外的成本,例如查询钥匙等性能上的损耗。

  • 单线程化

    同步化的方法会让你的程序因为要同步并行的问题而慢下来,换句话说,同步化会强制线程排队等着执行方法,你要想想你最开始为什么要写多线程并行的程序。

  • 死锁

    最可怕的是同步化可能会导致死锁现象。

原则上最好只做最少量的同步化。

总结

  • 如果两个或以上的线程存取堆上相同的对象可能会出现严重的问题,可能引发数据的损毁。

  • 要让对象在线程上有足够的安全性,就要判断哪些指令不能被分割执行。

  • 每个对象都有单一的锁,单一的钥匙,这只会在对象带有同步化的方法时才有实际的用途。

  • 对象就算是有多个同步化的方法,也还是只有一个锁。一旦某个线程进入该对象的同步化方法,其他线程就无法进入该对象上任何的同步化方法。

Comments !