线程的引入:cpu的执行效率比磁盘IO及网络快很多,为了提高cpu的利用率引入了线程。在等待IO及网络时,线程不再占用cpu周期把它让给其它线程。
线程概念:线程表示单独的执行流,拥有自己的程序执行计数器,有自己的栈。所谓栈即线程执行时当时变量的快照(共享变量可能被其它线程更改值)。
线程特性:
①线程通信。比如Object的中wait(),notify(),notifyAll();Reentrantlock内部类Condition的await(),signal(),signalAll();常用同步类CountDownLatch,CyclicBarrier,Semaphore;阻塞队列等。
②共享内存。共享内存,线程间就会产生竞争,竞争产生线程安全问题。java中采取的同步措施是显示锁及隐式锁。
单cpu与n个cpu:单个cpu上跑多个线程时,在同一时间只会有一个线程在跑,即只有一个线程获得cpu时钟周期。n个cpu可以实现同一时间n个线程同时跑。
线程不安全的原因
线程在改写变量之前需到主内存读变量的值。读到的值可能是其它线程没来得及从工作内存(下图的cache)写到主内存中。导致读到的是原来的值。采取同步措施保证线程在读变量之前一定读到的是另一线程改写后的值。
线程中常用方法
继承Thread、实现Runable、实现Callable接口都可创建一个新线程。语句必须依赖于线程才能执行。任意线程都能够创建新的线程。
Thread中静态方法
- yield()方法:试着让出cpu周期,不释放锁
- sleep()方法:线程休眠一段时间,不释放锁
- currentThread()方法:获取当前线程
- interrupted()方法:重置当前线程的中断状态
Thread中非静态方法
- start()方法:创建新的线程。
- run()方法:执行run方法在当前线程
- setName()方法:设置线程的名字
- getName()方法:获取线程的名字
- join()方法:等待其子线程执行完毕
- setDaemon():参数为true则设置线程为守护线程
- interrupt():尝试中断线程,取消任务时使用
- isInterrupted():线程是否中断
注意:
①每个线程对象只能调用一次start()方法
②setName、join、setDaemon、interrupt方法若调用则在start方法前调用
③线程只是start方法比较特殊,继承跟实现规则与普通类没有区别。因此在实现Thread、Runable时,子类可定义任意的属性及方法
run()方法与start()方法的区别
- 调用run方法不会开启一个新的线程,run方法会在当前的线程中执行。调用start方法会开启一个线程即开启单独的执行流,run方法会在新的线程执行。
- run方法一个线程可调用多次,start方法只能调用一次
- run方法共享时不会产生线程不安全问题
public class HelloRunnable implements Runnable {
//变量方法、随我们自由定义,跟继承其它类的规则相同
protected int i=0;
@Override
public void run() {
System.out.println("执行的线程名字:"+Thread.currentThread().getName());
try {
//休眠100毫秒为了模拟执行i++的时候尽可能多的并发
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i++);
}
public static void main(String[] args) {
HelloRunnable helloRunnable = new HelloRunnable();
for (int i=0;i<50;i++){
Thread thread = new Thread(helloRunnable);
thread.setName("helloRunnable");
//主线程执行完start方法后,run方法执行的语句顺序才可能比主线程先执行。
thread.start();//打印的线程名字为:helloRunnable
//把start注释掉换成thread.run()则不再时多线程。可以看到打印的线程名字为main
//thread.run();
}
}
}
yield、join、sleep方法的区别
① yield、sleep是static方法。join是实例方法。
②sleep、join方法必须捕获InterruptedException异常。会响应中断。
③各自方法的含义不同。
yield与join的区别。join等待其线程完成,yield只是尽可能让出cpu周期。使用join便会出现线程阻塞,使用yield线程继续执行。
public class YieldTest implements Runnable{
private String name;
public YieldTest(String name) {
this.name = name;
}
public void run() {
for (int i = 1; i < 20; i++) {
System.out.println(name + ":" + i);
// Thread.yield() ;// 不需要捕获异常,尽可能的让出cpu周期
try {
Thread.currentThread().join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//只剩一个线程也可以,不是同一优先级的也可以
System.out.println("----");
}
}
/**
* 暂停当前正在执行的线程对象,并执行其他线程。
*
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(new YieldTest("t1"));
t1.setName("测试");
t1.setPriority(3);
Thread t2 = new Thread(new YieldTest("t2"));
t2.setPriority(8);
t1.start();
t2.start();
System.out.println("main");
}
}
线程的优点及成本
为什么要创建单独的执行流?或者说线程有什么优点呢?至少有以下几点:
- 充分利用多CPU的计算能力,单线程只能利用一个CPU,使用多线程可以利用多CPU的计算能力。
- 充分利用硬件资源,CPU和硬盘、网络是可以同时工作的,一个线程在等待网络IO的同时,另一个线程完全可以利用CPU,对于多个独立的网络请求,完全可以使用多个线程同时请求。
- 在用户界面(GUI)应用程序中,保持程序的响应性,界面和后台任务通常是不同的线程,否则,如果所有事情都是一个线程来执行,当执行一个很慢的任务时,整个界面将停止响应,也无法取消该任务。
- 简化建模及IO处理,比如,在服务器应用程序中,对每个用户请求使用一个单独的线程进行处理,相比使用一个线程,处理来自各种用户的各种请求,以及各种网络和文件IO事件,建模和编写程序要容易的多。
成本
关于线程,我们需要知道,它是有成本的。创建线程需要消耗操作系统的资源,操作系统会为每个线程创建必要的数据结构、栈、程序计数器等,创建也需要一定的时间。
此外,线程调度和切换也是有成本的,当有当量可运行线程的时候,操作系统会忙于调度,为一个线程分配一段时间,执行完后,再让另一个线程执行,一个线程被切换出去后,操作系统需要保存它的当前上下文状态到内存,上下文状态包括当前CPU寄存器的值、程序计数器的值等,而一个线程被切换回来后,操作系统需要恢复它原来的上下文状态,整个过程被称为上下文切换,这个切换不仅耗时,而且使CPU中的很多缓存失效,是有成本的。
另外,如果执行的任务都是CPU密集型的,即主要消耗的都是CPU,那创建超过CPU数量的线程就是没有必要的,并不会加快程序的执行。参考:
总结:
①程序的执行必须依附在线程上,使用Thread.currentThread()获取当前执行程序的线程。
②线程通信是多个线程之间通信,比如wait()、await()方法时阻塞的是当前线程,需要其它线程对其唤醒。
③内存共享导致的线程不安全问题,每个线程的工作内存都是独立的,需要刷到主内存。
④方法参数的基本类型及String类型是线程安全的,在方法中new 的对象也是安全的(不返回new的对象)。只要没有内存共享即是安全的,利用这个原则可以减少同步代码的语句。
每篇一语:
在我的后园,可以看见墙外有两株树,一株是枣树,还有一株也是枣树。——《秋夜》鲁迅