1.线程和线程池
1.1线程类创建的两种方法:
//方法一:继承Thread 类,实现run方法。
class ThreadTest extends Thread {
ThreadTest() {
}
public void run() {
// code here ...
}
}
//创建并启动一个线程:
ThreadTest t = new ThreadTest();
t.start();
//方法二:实现 Runnable 接口的类,实现run方法。
class RunTest implements Runnable {
RunTest () {
}
public void run() {
// code here . . .
}
}
//创建并启动一个线程:
RunTest r = new RunTest(143);
new Thread(r).start();
1.2线程池
在java开发中,创建一个线程常用的方法是new一个Thread,但是new Thread却有很多弊端。先说说new Thread的弊端:
new Thread(new Runnable(){
@Override
public void run(){
// TODO Auto-generated method stub
}
}).start();
这是一个再简单不过的例子了,但如果你有许多需要长时间运行的任务同时执行,并需要等所有的这些线程都执行完毕,还想得到一个返回值,那么这就有点小小难度了。要是你还用以上方法new Thread,那你就out掉了。这就是他的弊端,除此之外还有:
1. 每次new Thread新建对象性能差。
2. 线程缺乏统一管理,可能无限制新建线程,相互之间存在竞争,及可能占用过多系统资源导致死机或OOM。
3. 缺乏更多功能,如定时执行、定期执行、线程中断。
没有其他解决方案了吗?NO,Java 已经有解决方案给你,那就是Executors,一个简单的类可以让你创建线程池和线程工厂。Java提供的四种线程池,它的好处在于:
1. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
2. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
3. 提供定时执行、定期执行、单线程、并发数控制等功能。
Java通过Executors提供四种线程池,一个线程池使用类ExecutorService的实例来表示,通过 ExecutorService 你可以提交任务,并进行调度执行,先看看Java的四种线程池。
1.newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程池里有很多线程需要同时执行,老的可用线程将被新的任务触发重新执行,如果线程超过60秒内没执行,那么将被终止并从池中删除,代码:Executors.newCachedThreadPool()。
2.newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。如果没有任务执行,那么线程会一直等待,代码: Executors.newFixedThreadPool()。
3.newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。用来调度即将执行的任务的线程池,代码:Executors.newScheduledThreadPool()。
4.newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
1)newCachedThreadPool的示例代码:
线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
try {
Thread.sleep(index * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(index);
}
});
}
2)newFixedThreadPool的示例代码:
因为线程池大小为3,每个任务输出index后sleep 2秒,所以每两秒打印3个数字。定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
3) newScheduledThreadPool的示例代码:
表示延迟3秒执行。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("delay 3 seconds");
}
}, 3, TimeUnit.SECONDS);
4) newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。示例代码如下:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
}
结果依次输出,相当于顺序执行各个任务。
现行大多数GUI程序都是单线程的。Android中单线程可用于数据库操作,文件操作,应用批量安装,应用批量删除等不适合并发但可能IO阻塞性及影响UI线程响应的操作。
2.线程和线程的关系及区别
1.定义
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源.
2.关系
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.
相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
3.区别
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
1)简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
2)线程的划分尺度小于进程,使得多线程程序的并发性高。
3)另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
4)线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
5)从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
4.优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。同时,线程适合于在SMP机器上运行,而进程则可以跨机器迁移。
3.线程竞争
资源竞争 (线程互斥)
3.1什么是资源竞争
有这样一种资源,在某一时刻只能被一个线程所使用:比如打印机、某个文件等等,如果多个线程不加控制的同时使用这类资源,必然会导至错误。
下面的例子模拟了一个打印机,多个线程不加控制的同时使用这个打印机:
1.public class Printer
2.{
3. public void print(int printer, String content)
4. {
5. System.out.println("Start working for [" +printer+"]");
6. Thread.yield();
7. System.out.println("===================");
8. Thread.yield();
9. System.out.println(content);
10. Thread.yield();
11. System.out.println("===================");
12. Thread.yield();
13. System.out.println("Work complete for [" +printer+"]\n");
14. }
15. public static void main(String[] args)
16. {
17. Printer p = new Printer();
18.
19. for (int i=0; i<3; i++)
20. new Thread(new MyThread(p)).start();
21. }
22.}
23.
24.class MyThread implements Runnable
25.{
26. private static int counter = 0;
27. private final int id = counter++;
28.
29. private Printer printer;
30.
31. public MyThread(Printer printer)
32. {
33. this.printer = printer;
34. }
35.
36. @Override
37. public void run()
38. {
39. printer.print(id, "Content of " + id);
40. }
41.}
输出结果(sample)
1.Start working for [0]
2.Start working for [1]
3.===================
4.===================
5.Start working for [2]
6.Content of 0
7.Content of 1
8.===================
9.===================
10.===================
11.Content of 2
12.Work complete for [0]
13.
14.Work complete for [1]
15.
16.===================
17.Work complete for [2]
从结果可以看到,打印机的输出完全乱套了,各个线程想要打印的内容全部参杂在一起了。
3.2解决资源竞争问题
原则上要解决这类问题并不难,只需要一个锁的机制。任何线程在使用打印机前必须先对打印机上锁;在使用完打印机后释放锁;如果线程尝试对打印机上锁时别的线程已经上了锁,则该线程必须等待别的线程先释放锁。
Java中,解决上述资源共享类的问题是通过关键字synchronized实现的。java中的对象都有一个‘锁’,这样,任何一个线程尝试访问对象的synchronized方法时,必须要先获得对象的'锁',否则必须等待。
一个对象可能会有多个synchronized方法,比如synchronized a()方法和synchronized b()方法。当一个线程获得了对象的锁,进行a()方法、或b()方法了,那么在线程释放该对象的锁之前,别的线程是不能访问该对象的其它synchronized方法的。
下面例子是之前的例子的改良版本,只需要简单的把Printer对象的print方法定义成synchronized的就可以达到我们的要求了:
1.public class Printer
2.{
3. public synchronized void print(int printer, String content)
4. {
5. System.out.println("Start working for [" +printer+"]");
6. Thread.yield();
7. System.out.println("===================");
8. Thread.yield();
9. System.out.println(content);
10. Thread.yield();
11. System.out.println("===================");
12. Thread.yield();
13. System.out.println("Work complete for [" +printer+"]\n");
14. }
15.
16. public static void main(String[] args)
17. {
18. Printer p = new Printer();
19.
20. for (int i=0; i<3; i++)
21. new Thread(new MyThread(p)).start();
22. }
23.}
24.
25.class MyThread implements Runnable
26.{
27. private static int counter = 0;
28. private final int id = counter++;
29.
30. private Printer printer;
31.
32. public MyThread(Printer printer)
33. {
34. this.printer = printer;
35. }
36.
37. @Override
38. public void run()
39. {
40. printer.print(id, "Content of " + id);
41. }
42.
43.}
输出结果 :
1.Start working for [0]
2.===================
3.Content of 0
4.===================
5.Work complete for [0]
6.
7.Start working for [2]
8.===================
9.Content of 2
10.===================
11.Work complete for [2]
12.
13.Start working for [1]
14.===================
15.Content of 1
16.===================
17.Work complete for [1]
从结果的输出可以看出来,被模拟的打印机资源在某一时刻,仅被一个线程所使用。
3.3临界区
有些时候,你可能不需要隔离整个方法,而只需要隔离方法中的部分代码,这部分被隔离的代码就叫做临界区。临界区中的代码在某一时刻,只能被一个线程访问。
下面的例子,是用临界区的方式实现了前面的例子:
1.public class Printer
2.{
3. public void print(int printer, String content)
4. {
5. synchronized(this)
6. {
7. System.out.println("Start working for [" +printer+"]");
8. Thread.yield();
9. System.out.println("===================");
10. Thread.yield();
11. System.out.println(content);
12. Thread.yield();
13. System.out.println("===================");
14. Thread.yield();
15. System.out.println("Work complete for [" +printer+"]\n");
16. }
17. }
18.
19. public static void main(String[] args)
20. {
21. Printer p = new Printer();
22.
23. for (int i=0; i<3; i++)
24. new Thread(new MyThread(p)).start();
25. }
26.}
27.
28.class MyThread implements Runnable
29.{
30. private static int counter = 0;
31. private final int id = counter++;
32.
33. private Printer printer;
34.
35. public MyThread(Printer printer)
36. {
37. this.printer = printer;
38. }
39.
40. @Override
41. public void run()
42. {
43. printer.print(id, "Content of " + id);
44. }
45.
46.}
可以看到,在Java中临界区也是通过synchronized关键字实现的。在synchronized关键字后面,要传一个对象参数,任何线程要进入临界区时必须先要获得该对象的锁,退出临界区时要释放该对象的锁,这样别的线程才有机会进入临界区。
从上面两个例子可以看出,临界区和synchronized方法,其原理都是一样的,都是通过在对象上加锁来实现的,只不过临界区来得更加灵活,因为它不光可以对this对象加锁,也可以对任何别的对象加锁。
3.4 Lock
Java1.5提供了一个显示加锁的机制,比起synchronized方式来说,显示加锁的方法可能让代码看上去更加复杂,但是也带来了更好的灵活性。
下面的例子,用Lock的机制实现了前面的例子:
1.public class Printer
2.{
3. private Lock lock = new ReentrantLock();
4.
5. public void print(int printer, String content)
6. {
7. lock.lock();
8.
9. try
10. {
11. System.out.println("Start working for [" +printer+"]");
12. Thread.yield();
13. System.out.println("===================");
14. Thread.yield();
15. System.out.println(content);
16. Thread.yield();
17. System.out.println("===================");
18. Thread.yield();
19. System.out.println("Work complete for [" +printer+"]\n");
20. }
21. finally
22. {
23. lock.unlock();
24. }
25. }
26.
27. public static void main(String[] args)
28. {
29. Printer p = new Printer();
30.
31. for (int i=0; i<3; i++)
32. new Thread(new MyThread(p)).start();
33. }
34.}
35.
36.class MyThread implements Runnable
37.{
38. private static int counter = 0;
39. private final int id = counter++;
40.
41. private Printer printer;
42.
43. public MyThread(Printer printer)
44. {
45. this.printer = printer;
46. }
47.
48. @Override
49. public void run()
50. {
51. printer.print(id, "Content of " + id);
52. }
53.
54.}
使用Lock的时候,必须要注意两点:
·锁的释放必须放在finally块里面,以保证锁被正确的释放;
·如果被隔间的方法或临界间需要返回一个值,那么return语句应该放在try块中,从而不至于使unlock发生得过早而导致错误的发生。
使用显示的Lock机制,可以让程序更加的灵活。比如上面的例子中,如果尝试使用打印机的时候,打印机正被别的线程所使用,那么早取消本次打印。要实现这样的功能,使用synchronized可能不太容易实现,但是使用Lock机制的话,就非常简单了:
1.public class Printer
2.{
3. private Lock lock = new ReentrantLock();
4.
5. public void print(int printer, String content)
6. {
7. boolean isLocked = lock.tryLock();
8.
9. if (!isLocked)
10. return;
11.
12. try
13. {
14. System.out.println("Start working for [" +printer+"]");
15. Thread.yield();
16. System.out.println("===================");
17. Thread.yield();
18. System.out.println(content);
19. Thread.yield();
20. System.out.println("===================");
21. Thread.yield();
22. System.out.println("Work complete for [" +printer+"]\n");
23. }
24. finally
25. {
26. if(isLocked)
27. lock.unlock();
28. }
29. }
30.
31. public static void main(String[] args)
32. {
33. Printer p = new Printer();
34.
35. for (int i=0; i<3; i++)
36. new Thread(new MyThread(p)).start();
37. }
38.}
39.
40.class MyThread implements Runnable
41.{
42. private static int counter = 0;
43. private final int id = counter++;
44.
45. private Printer printer;
46.
47. public MyThread(Printer printer)
48. {
49. this.printer = printer;
50. }
51.
52. @Override
53. public void run()
54. {
55. printer.print(id, "Content of " + id);
56. }
57.
58.}
Lock.tryLock()方法尝试对lock对象加锁并返回一个boolean值,如果成功了,返回true,表明当前没有别的线程在使用打印机,那么当前线程将获得lock对象的锁,并继续打印;如果失败了,返回false,表明别的线程正在使用打印机,当前线程将简单的返回,而不是等待别的线程释放锁。