一、线程与进程
(一)进程产生的原因:
由于cpu与其他pc资源(RAM,显卡、光驱、键盘等)之间速度的不协调,为了提高资源利用率,提出了多任务系统,得益于cpu的计算速度,实质上是多个任务轮流使用cpu资源,由于速度很快,感觉是同时进行的。
任务执行需要具备执行环境,即程序执行所需的各种pc资源(显卡、RAM等),将其称之为程序上下文,要实现程序可以同时执行,就需要不断轮换,为了从当前状态继续执行下去,计算机需要保存之前的程序上下文,因此有了进程,用进程来表述程序当前上下文的状态信息(内存信息,变量值,任务ID等)。
因此,程序在运行时,需要加载程序上下文,执行程序,保存程序上下文,调入另一个程序的上下文,执行程序。。。
(二)线程产生的原因:
当程序增多时,耗时就会增大,为了进一步提高资源利用率,在进程中,引入了线程,线程只是cpu轮流调度的单位。例如:要运行程序A,实际上分成a,b,c等多个块组合而成,具体执行情况:程序A得到CPU——》CPU加载程序上下文,开始执行程序A的a小段,b小段,c小段,最后cpu保存A的上下文。这里a,b,c的执行共享了A的上下文,其中a,b,c就是线程,即更加细分了CPU的使用时间段。
(三)进程的概念
进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。
注意:程序是一个静态的概念,机器上的一个class文件或者exe文件就是一个程序。进程是程序的一次动态执行,首先将class文件加载到代码区,这时进程还没有开始执行,但进程已经产生了。平常说的进程的执行指的是主线程执行即main()方法的执行。
(四)线程的概念
线程:线程是进程中的一个执行流程,同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(主要用于解决共享资源的有效有序使用,保证运行结果正确可控)。
线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
二、线程的创建与执行
线程有三种创建方法:
(一)第一种方法:继承Thread类
继承java.lang.Thread类,子类必须重写run()方法,该方法作为新线程的入口点,同时,必须调用start()方法才能执行。(start()方法是通知cpu,线程我准备好了)
构造方法:publicThread(Runnable target)
注意:Thread类实际上是实现了Runnable接口的类。
(二)第二种方法:实现Runnable接口(推荐)
实现java.lang.Runnable接口,实现public void run()方法,该方法用于定义线程的运行体。(推荐使用)
注意:使用Runnable接口可以为多个线程提供共享的数据。
在run()方法中可以使用Thread类的静态方法:currentThread()方法:获取当前线程的引用
(三)第三中方法:利用Callable接口和FutureTask类实现
-
1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
-
2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
-
3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
-
4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
public class CallableThreadTest implements Callable<Integer> {
public static void main(String[] args)
{
CallableThreadTest ctt = new CallableThreadTest();
FutureTask<Integer> ft = new FutureTask<>(ctt);
for(int i = 0;i < 100;i++)
{
System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);
if(i==20)
{
new Thread(ft,"有返回值的线程").start();
}
}
try
{
System.out.println("子线程的返回值:"+ft.get());
} catch (InterruptedException e)
{
e.printStackTrace();
} catch (ExecutionException e)
{
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception
{
int i = 0;
for(;i<100;i++)
{
System.out.println(Thread.currentThread().getName()+" "+i);
}
return i;
}
}
(四)总结:
-
1. 采用实现 Runnable、Callable 接口的方式创见多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。
-
2. 使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。
三、线程的状态
(一)线程状态
1、线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。
- 新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
- 就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
- 运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
- 阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
-
等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
-
同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
-
其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
-
- 死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
(二)线程控制方法:
1、isAlive()方法:判断线程是否还活着,即线程是否终止
2、getPriority()方法:获得线程的优先级数值。
3、setPriority()方法:设置线程的优先级数值。
注意:线程的优先级无法保证线程的执行次序,只不过,优先级高的线程获得CPU资源的概率比较大,获得CPU的执行时间越多。优先级低的也有机会执行。优先级用1-10之间的整数表示,默认是5(可省略),数值越大,优先级越高。
4、Thread.sleep():将当前线程睡眠指定毫秒数
5、join()方法:调用某线程的该方法,将当前线程和该线程合并,即等待该线程结束,再恢复当前线程的运行
6、yield()方法:让出CPU,当前线程进入就绪队列等待调度。
7、wait()方法:当前线程进入阻塞状态,让出CPU控制权,当前线程进入对象的wait pool。
8、notify()方法、notifyAll()方法:唤醒对象的wait pool 中的一个或者所有等待的线程
(三)sleep()和wait()的区别(重点)
1、每个对象都有一个锁来控制同步访问,Synchronized关键字可以和对象的锁交互,来实现同步方法或同步块。sleep()方法正在执行的线程主动让出CPU(然后CPU就可以去执行其他任务),在sleep指定时间后CPU再回到该线程继续往下执行(注意:sleep方法只让出了CPU,而并不会释放同步资源锁!!!);wait()方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了notify()方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行。(注意:notify的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说notify只是让之前调用wait的线程有权利重新参与线程的调度);
2、sleep()方法可以在任何地方使用;wait()方法则只能在同步方法或同步块中使用;
3、sleep()是线程线程类(Thread)的方法,调用会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自动恢复;wait()是Object的方法,调用会放弃对象锁,进入等待队列,待调用notify()/notifyAll()唤醒指定的线程或者所有线程,才会进入锁池,不再次获得对象锁才会进入运行状态;
(四)sleep / join / yield 方法
1、sleep(long millis)方法:是Thread类的静态方法,睡眠指定的毫秒数。一秒=1000毫秒
注意:sleep(long millis)方法是静态方法,在那个线程中调用该方法,就让那个线程睡眠。
(1)interrupt()方法:中断线程,会抛出一个异常,进行处理(太粗暴,不推荐使用)
(2)stop()方法:中断线程,更加粗暴,直接杀死线程。(已过时,尽量不要使用)(重点)
注意:中断是一个状态,interrupt()方法只是将这个状态置为true而已。它并不像stop()方法那样会中断一个正在运行的线程,线程会不时地检测中断标识位,以判断线程是否应该被中断(中断标识值是否为true)。终端只会影响到wait状态、sleep状态和join状态。被打断的线程会抛出InterruptedException。
Thread.interrupted()检查当前线程是否发生中断,返回boolean
synchronized在获锁的过程中是不能被中断的。
因此,正常运行的程序不去检测状态,就不会终止,而wait等阻塞方法会去检查并抛出异常。如果在正常运行的程序中添加while(!Thread.interrupted()) ,则同样可以在中断后离开代码体
控制线程停止的方法:在线程类中定义一个Boolean类型的属性,对于while()循环,将其Boolean类型的属性设为false即可。
2、join()方法:优先执行完当前线程
join()方法:表示先执行完该线程(即调用join方法的线程),再执行其他线程。
3、yield()方法:暂停当前正在执行的线程对象,并执行其他线程
四、线程同步(重点)
共享资源一定要设置好同步,防止其他未设置同步的方法操作共享资源,引发冲突,造成数据错误(重点)。
(一)线程同步方法
JDK1.5之前,采用关键字synchronized和监视器机制来实现线程同步,JDK1.5之后,采用锁的机制实现线程同步。
1、 synchronized 修饰非静态方法,此时在这个方法体内的就有了 ”监视器“,它是所在类的对象.
synchronized void func(){}
2、 synchronized 修饰静态方法,此时在这个方法体内的也有了 ”监视器“,它是所在类的对象.
static synchronized void func(){}
3、 synchronized 修饰语句块,此时需要一个参数,而这个参数就作为“ 监视器 ”。
synchronized(Object o){ }
监视器就是一个对象,不过他是有条件的,线程获取监视器才能执行synchronized修饰的方法或者代码。
java.util.concurrent.locks包提供了锁和等待条件的接口和类, 可用于替代JDK1.5之前的同步(synchronized)和监视器机制(主要是Object类的wait(), notify(), notifyAll()方法).
(二)锁的机制概念
1、所谓互斥锁, 指的是一次最多只能有一个线程持有的锁. 在jdk1.5之前, 我们通常使用synchronized机制控制多个线程对共享资源的访问.
而现在, Lock提供了比synchronized机制更广泛的锁定操作。
2、Lock和synchronized机制的主要区别:
- synchronized机制提供了对与每个对象相关的隐式监视器锁的访问, 并强制所有锁获取和释放均要出现在一个块结构中, 当获取了多个锁时, 它们必须以相反的顺序释放. synchronized机制对锁的释放是隐式的, 只要线程运行的代码超出了synchronized语句块范围, 锁就会被释放.
- Lock机制必须显式的调用Lock对象的unlock()方法才能释放锁, 这为获取锁和释放锁不出现在同一个块结构中, 以及以更自由的顺序释放锁提供了可能. 以下代码演示了在不同的块结构中获取和释放锁。
(三)线程同步相关概念
1、线程同步:线程可能和其他线程共享一些资源,比如:文件,数据,内存等。当多个线程同时读写共享资源时,就可能发生冲突,为了解决这个问题,引入了线程同步机制,线程同步的真实意思,其实是“排队”:几个线程之间要排队,依次对共享资源进行操作,而不是同时进行操作。保证数据的准确性和唯一性,实现线程安全。
2、线程安全:多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问,必须等该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
注意:对全局变量和静态变量操作在多线程模型中会引发线程安全问题。
3、并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。相当于两个人同时做事情。
4、并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。相当于两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流进行,由于时间间隔较短,使人感觉两个任务都在运行。
注意:需要将事务和线程同步区分开,二者是不同层面的术语,应用场景不同。
多线程为了提高应用的执行效率而设置的,事务为保证一个操作的原子性而设置的
形象说明:多线程相当于多个人同时工作,在工作的过程中,大家可能都需要上厕所(厕所只有一个),这时就需要进行排队,每个人依次上厕所,第一个上厕所的人需要上锁,防止其他人进入,用完解锁,下一个人再用。这就是多线程和线程同步。
(四)使用synchronized实现线程同步
synchronized关键字的用法(是关键字,内置特性)(重点)
1、synchronized的用法(作用:同一时间只能一个线程执行synchronized修饰的代码或者方法)
synchronized代码块使用格式:synchronized(synObject){ //程序代码 }
注意:synObject可以是this,代表获取当前对象的锁,也可以是类中的一个属性,代表获取该属性的锁。
synchronized方法:就是将synchronized放在方法声明中,表示整个方法是同步方法。
例如:public synchronized void add(String str){ //方法体 }
注意:每个类也会有一个锁(类锁),它可以用来控制对static数据成员的并发访问。
(五)通过lock实现线程同步
1、Lock接口的用法(ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。)
问题:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。
但是采用synchronized关键字来实现同步的话,就会导致一个问题:
如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。
因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。
注意:Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
其中lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。
<1>.lock():如果锁已被其他线程获取,则进行等待。采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。
<2>.tryLock():有返回值,表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit): 和tryLock()方法是类似的,区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
<3>.lockInterruptibly()
获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用thread B.interrupt()方法能够中断线程B的等待过程。
注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。
因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。
(六)总结:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。
(七)死锁示例
类似案例:五个科学家吃放,互相占用筷子,最后饿死。
对于多线程,最基本的原则是:线程安全大于性能,一定要保证线程安全。
public class DealThread implements Runnable{
public int flag = 1;
static static Object o1 = new Object(),o2 = new Object(); //注意这里是静态的对象
@Override
public void run() {
System.out.println("flag="+flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (o1) {
System.out.println("0");
}
}
}
}
public static void main(String[] args) {
DealThread d1 = new DealThread();
DealThread d2 = new DealThread();
d1.flag = 1;
d2.flag = 0;
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d2);
t1.start();
t2.start();
}
}
//注意这里是静态的对象
@Override
public void run() {
System.out.println("flag="+flag);
if (flag == 1) {
synchronized (o1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (o2) {
System.out.println("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (o1) {
System.out.println("0");
}
}
}
}
public static void main(String[] args) {
DealThread d1 = new DealThread();
DealThread d2 = new DealThread();
d1.flag = 1;
d2.flag = 0;
Thread t1 = new Thread(d1);
Thread t2 = new Thread(d2);
t1.start();
t2.start();
}
}
参考文档:http://blog.csdn.net/g893465244/article/details/52538598