一、并发和并行
1.1 概念
- 并行:多个CPU实例或是多台机器同时执行一段处理逻辑,是真正的同时。
- 并发:通过CUP调度算法,让用户看上去同时去执行,实际上从CPU操作层面并不是真正的同时。并发往往需要公共的资源,对公共资源的处理和线程之间的协调是并发的难点。
1.2 比较
如果你想要一个程序运行的更快,那么可以将其断开为多个片段,在单独的处理器上运行每个片段。并发是用于多处理器编程的基本工具。为了使程序运行得更快,你必须学习如何利用这些额外的处理器,而则正是并发赋予你的能力。
如果你有一台多处理器的机器,那么就可以在这些处理器之间分部多个任务,从而可以极大的提高吞吐量。这是使用强有力的多处理器Web服务器的常见情况,在为每一个请求分配一个线程的程序中,它可以将大量的用户请求分布到多个CPU上。
但是,并发通常是提高运行在单处理器上的程序的性能。
因为,在单处理器上运行的并发程序开销应该比改程序的所有部分都顺序执行的开销大,因为其中增加了所谓的上下文切换的代价(从一个任务切换到另一个任务)。表面上看,将程序的所有部分当作单个的任务运行好像是开销更小一点,并且可以节省上下文切换的代价。
使得这个问题变得有些不同的是阻塞。如果程序中的某个任务因为该程序控制范围之外的某系条件(通常是I/O)而导致不能继续执行,那么我们就说这个任务或现场阻塞了。如果没有并发,则整个程序都将停止下来,直到外部条件发生变化。但是,如果使用并发来编写程序,那么当一个任务阻塞时,程序中的其他任务还可以继续执行,因此这个程序可以保持继续向前执行。事实上,从性能的角度看,如果没有任务阻塞,那么在单处理器上使用并发就没有任何意义。
在单处理器系统中的性能提高的常见实例是事件驱动的编程。实际上,使用并发最吸引人的一个原因就是要产生具有可响应的用户界面。这也是在Android系统中不允许子线程总访问UI的原因,在Android中的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态。那为什么系统不对UI控件的访问加上锁机制呢?原因有两个:其一,加上锁会让UI访问的逻辑变的复杂;其二,锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。所以在Android系统中最简单而且高效的方法就是采用单线程模型来处理UI操作。
1.3 进程和线程
实现并发最直接的方式是在操作系统级别使用进程。进程是运行在它自己的地址空间内的自包容的程序。多任务操作系统可以通过周期性地将CPU从一个进程切换到另一个进程,来实现同时运行多个(进程)程序,尽管这使得每个进程看起来在其执行过程中都是歇歇停停。进程被操作系统互相隔开,因此不会彼此干涉,这使得用进程编程相对容易一些。与此相反,像Java所使用的这种并发系统会共享诸如内存和I/O这样的资源,因此编程编写多线程程序最基本的困难在于,协调不同线程驱动的任务之间对这些资源的使用,以便使得这些资源不会同时被多个任务访问。
二、基础概念
2.1 线程状态图
线程包括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()方法,该线程结束生命周期。
2.2 关键函数和关键字
这5中状态涉及到的内容包括Object类,Thread类和synchronized关键字。
在Object类中,定义了wait(),notify(),notifyAll()等一系列休眠/唤醒函数。
在Thread类中,定义了一系列线程操作函数,例如,sleep()休眠函数,interrupt中断()函数,getName()获取线程名称等。
synchronized关键字,它是区分synchronized代码块和synchronized方法。synchronized的作用是让线程获取对象的同步锁。
怎么样才能让Java里的线程安全停止工作呢
线程自然终止:自然执行完或抛出未处理异常
stop(),resume(),suspend()已不建议使用,stop()会导致线程不会正确释放资源,suspend()容易导致死锁。
java线程是协作式,而非抢占式
调用一个线程的interrupt() 方法中断一个线程,并不是强行关闭这个线程,只是跟这个线程打个招呼,将线程的中断标志位置为true,线程是否中断,由线程本身决定。
isInterrupted() 判定当前线程是否处于中断状态。
static方法interrupted() 判定当前线程是否处于中断状态,同时中断标志位改为false。
方法里如果抛出InterruptedException,线程的中断标志位会被复位成false,如果确实是需要中断线程,要求我们自己在catch语句块里再次调用interrupt()。
三、常用的实现多线程的两种方式
3.1 概述
常用的实现多线程的两种方式:Thread和Runnable。之所以说是“常用”,是因为在Java 5后可以通过java.util.concurrent包中的线程池来实现多线程。Runnable是一个接口,该接口包含了一个run()方法,Runnable具有更好的扩展性。Thread是一个类,Thread本身就是实现了Runnable接口。此外,Runnable还可以用于“资源共享”。即,多个线程都是基于某个Runnable对象建立的,它们会共享Runnable对象上的资源。
3.2 实现多线程实例
3.2.1 实现Runnable接口
//多线程方法二 实现Runnable类
public class TicketRun 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.2.2 继承Thread
//多线程方法一 继承Thread 类
public class TicketThread 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--);
}
}
}
}
//多线程方法三 实现Callable类 有返回值
public class TicketCallable implements Callable<String>{
private int ticket=10;
@Override
public String call() throws Exception {
for(int i=0;i<20;i++){
if(this.ticket>0){
System.out.println(Thread.currentThread().getName()+" 卖票:ticket "+this.ticket--);
}
}
return ticket+"";
}
}
测试:
@Test
public void testThread() throws InterruptedException, ExecutionException {
TicketThread ticketThread = new TicketThread();
Thread thread1 = new Thread(ticketThread);
thread1.start();
Thread.sleep(1000L);
TicketRun ticketRun= new TicketRun();
Thread thread2 = new Thread(ticketRun);
//设置为守护线程
//thread2.setDaemon(true);
thread2.start();
Thread.sleep(1000L);
//测试Callable
TicketCallable ticketCallable= new TicketCallable();
FutureTask<String> futureTask = new FutureTask<String>(ticketCallable);
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
四、synchronized关键字
在Java中,每个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在的。当我们调用某个对象的synchronized方法时,就获得了该对象的同步锁,不同线程对同步锁的访问是互斥的。因为锁语句产生了一种互相排斥的效果,所以这种机制常常称为互斥量(mutex)。
对象锁,锁的是类的对象实例。
类锁 ,锁的是每个类的的Class对象,每个类的的Class对象在一个虚拟机中只有一个,所以类锁也只有一个。
4.2 synchronized基本原则和实例
4.2.1 基本原则
我们将synchronized的基本规则总结为下面3条,并通过实例对它们进行说明。
第一条:当一个线程访问某对象的synchronized方法或者synchronized代码块时,其他线程对该对象的该synchronized方法或者synchronized代码块的访问将被阻塞。
第二条:当一个线程访问某对象的synchronized方法或者synchronized代码块时,其他线程仍然可以访问该对象的非同步代码块。
第三条:当一个线程访问某对象的synchronized方法或者synchronized代码块时,其他线程对该对象的其他的synchronized方法或者synchronized代码块的访问将被阻塞。
4.2.2 实例
第一条
:当一个线程访问某对象
的synchronized方法
或者synchronized代码块
时,其他线程对该对象
的该synchronized方法
或者synchronized代码块
的访问将被阻塞。
package com.dukun.bingfa.day01;
//实例1
public class RunnableTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
class MyRunnable implements Runnable{
@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();
}
}
结果:
结果说明:run()方法中存在synchronized(this)代码块,而且t1和t2都是基于MyRunnable这个Runnable对象创建的线程。这就意味着,我们可以将synchronized(this)中的this看做是MyRunnable这个Runnable对象;因此,线程t1和t2共享“MyRunable对象的同步锁”。所以,当一个线程运行的时候,另外一个线程必须等待正在运行的线程释放MyRunnable的同步锁之后才能运行。
下面有个实例是针对两个对象的:
package com.dukun.bingfa.day01;
//实例1
public class RunnableTest {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
class MyRunnable implements Runnable{
@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 runnable1 = new MyRunnable();
Runnable runnable2 = new MyRunnable();
Thread t1 = new Thread(runnable1,"t1");
Thread t2 = new Thread(runnable2,"t2");
t1.start();
t2.start();
}
}
运行结果:
对比结果,你是否可以正确且快速的明白呢?实例1的结果是先执行线程t1然后才是线程t2,实例2的结果是线程t1和t2交替执行。
分析:synchronized(this)中的this是指当前对象,即synchronized(this)所在类对应的当前对象。它的作用是获取获取当前对象的同步锁。对于实例2中的synchronized(this)中的this代表的是MyRunnable 对象,t1和t2是两个不同的MyRunnable 对象,因此t1和t2在执行synchronized(this)时获取的是不同对象的同步锁。对于实例1来说,synchronized(this)中的this代表的时候MyRunnable 对象,t1和t2是共同一个MyRunnable对象,因此,一个线程获取了对象的同步锁,会造成另一个线程的等待。
————————————————————————————————————————————————————————
第二条
:当一个线程访问某对象
的synchronized方法
或者synchronized代码块
时,其他线程仍然可以访问该对象
的非同步代码块。
package com.dukun.bingfa.day01;
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
}
}
运行结果:
结果说明:
线程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才能运行。
4.3 synchronized方法和synchronized代码块
4.3.1 概述
synchronized方法是用synchronized修饰方法,这是一种粗粒度锁;这个同步方法(非static方法)无需显式指定同步监视器,同步方法的同步监视器是this,也就是调用该方法的对象。
synchronized代码块是用synchronized修饰代码块,这是一种细粒度锁。线程开始执行同步代码块之前,必须先获得对同步监视器的锁定,任何时候只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对同步监视器的锁定。虽然Java允许使用任何对象作为同步监视器,但同步监视器的目的就是为了阻止两个线程对同一个共享资源进行并发访问,因此通常推荐使用可能被并发访问的共享资源充当同步监视器。
4.4 实例锁和全局锁
4.4.1 概念
实例锁:锁在某个实例对象上。如果该类是单例,那么该锁也是具有全局锁的概念。实例锁对应的就是synchronized关键字。
全局锁:该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。全局锁对应的就是static synchronized(或者是锁在该类的class或者classloader对象上)。