一、线程概述
1、进程:是一个正在执行中的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元,是操作系统实现多任务的基础。
2、线程:就是进程中的一个独立的轻量级的控制单元,线程在控制着进程的执行,一个进程中至少要有一个线程。
3、多线程:在更的低层次引入了多任务的思想,使一个进程里有多个线程,从而让单个程序看起来可以同时处理多个任务。
4、线程的优势:每个进程都有它自己的完备的变量集,线层则共享这些数据。这些共享的变量使线程之间的通信比进程之间的通信更加有效简单,而且,线程比进程更轻量级,创建和销毁单个线程比进程的开销要小的多。
5、java中一些关于线程的细节:
(1)、Java VM 启动的时候会有一个进程java.exe.该进程中至少一个线程负责java程序的执行。而且这个线程运行的代码存在于main方法中。该线程称之为主线程。
(2)、深究起来的话,jvm启动的不止一个线程,除了主线程,还有负责执行垃圾回的后台线程。
6、创建线程的方式:
(1)、定义Thread的子类,复写Thread类中的run方法,将自定义代码存储在run方法,然后创建线程对象,调用该对象的start方法,该方法有两个作用,启动线程和调用run方法。
(2)、定义Runnable的子类,复写Runnable类中的run方法,将自定义代码存储在run方法,然后创建Runnable子类对象,
以此对象作为Thread构造器的参数创建线程类对象,再调用线程的start方法,该方式可以实现数据共享。
第二种实现方式的优化分析:假设有两个存在继承关系的类,如下
父类A
class A{}
子类B
class B extends A{
public void execute(){
System.out.println("需要线程执行的方法");
}
}
这时候如果B中有一部分代码(假设就是execute()方法)需要放在线程中执行,就无法继承Thread类并把要执行的代码写入run方
法中了,因为java只支持单继承,所以,就出现了第二种方法,避免了单继承的局限性,如下这时候如果B中有一部分代码(假设就是execute()方法)需要放在线程中执行,就无法继承Thread类并把要执行的代码写入run方
法中了,因为java只支持单继承,所以,就出现了第二种方法,避免了单继承的局限性,如下这时候如果B中有一部分代码(假设就是execute()方法)需要放在线程中执行,就无法继承Thread类并把要执行的代码写入run方
法中了,因为java只支持单继承,所以,就出现了第二种方法,避免了单继承的局限性,如下这时候如果B中有一部分代码(假设就是execute()方法)需要放在线程中执行,就无法继承Thread类并把要执行的代码写入run方法中了,因为java只支持单继承,所以,就出现了第二种方法,避免了单继承的局限性,如下
class B extends A implements Runnable{
public void run(){
execute();
}
public void execute(){
System.out.println("需要线程执行的方法");
}
}
new Thread(new B()).start();
还有一种情况就是当多个线程需要共享数据时,使用第一种方法就需要定义一个静态变量来达到共享目的,如下
class C extends Thread{
private static int i = 100; //类属性
public void run(){
while(true){
if(i>0){
System.out.println(""+i--);
}else{
break;
}
}
}
}
这种情况下i的生命周期会很长,直到虚拟机停止运行,不利于内存优化,所以可以用第二种方式消除这个弊端,如下这种情况下i的生命周期会很长,直到虚拟机停止运行,不利于内存优化,所以可以用第二种方式消除这个弊端,如下
class C implements Runnable{
private int i = 100; //实例属性
public void run(){
while(true){
if(i>0){
System.out.println(""+i--);
}else{
break;
}
}
}
}
C c = new C();
new Thread(c).start();
new Thread(c).start();
new Thread(c).start();
7、run方法作用:Thread类用于描述线程,该类就定义了一个功能,用于存储线程要运行的代码,该功能就是run方法。也就是
说Thread类中的run方法,用于存储线程要运行的代码,叫做线程执行体。7、run方法作用:Thread类用于描述线程,该类就定义了一个功能,用于存储线程要运行的代码,该功能就是run方法。也就是说Thread类中的run方法,用于存储线程要运行的代码,叫做线程执行体。
二、线程状态
1、线程有五种状态:
(1)、新建状态,当使用new操作符创建一个线程时,线程还没有开始运行,此时线程就处于新建状态。
(2)、就绪状态,当调用了线程对象的start()方法以后,线程未必马上执行,要等到获取了CPU资源以后才可以,这时 线程就处于就绪状态。
(3)、运行状态,处于就绪状态的线程获得了CPU资源以后开始执行,就进入了运行状态。
(4)、阻塞状态,当发生以下任何一种情况线程就进入阻塞状态
①线程通过调用sleep进入睡眠状态
②线程调用一个在I/O上被阻塞的操作,而I/O操作还未完成
③线程视图得到一个正在被其他线程持有的锁
④线程在等待一个通知被唤醒
⑤调用了线程的susppend方法(已过时)
当一个线程被阻塞,另一个线程就可以运行啦,当一个被阻塞的线程再次被激活时就进入了就绪状态,这时调度器就会检查它的优先级是否高于当前运行的线程,如果是,他 就将抢占当前运行线程的资源并开始运行。通过以下途径可以让线程由阻塞状态回到就绪状态
①线程的sleep方法超时
②线程调用的在I/O上被阻塞的操作完成
③线程获得了正在等待的锁
④线程接收到了激活通知
⑤调用了线程的resume方法(已过时)
(5)、死亡状态,run方法正常退出或者未捕获异常终止了run方法,stop方法也会导致线程死亡,但已过时。
状态图:
三、多线程的安全与同步
1、首先以一个售票的例子引出多线程的安全问题
public class MulThreadSalTicketException
{
public static void main(String[] args)
{
Ticket t = new Ticket();
new Thread(t,"窗口一").start();
new Thread(t,"窗口二").start();
new Thread(t,"窗口三").start();
new Thread(t,"窗口四").start();
}
}
class Ticket implements Runnable{
private int i = 100;
public void run(){
while(true){
if(i>0){ //①
System.out.println(Thread.currentThread().getName()+"售出"+i--+"号票");
}else{
break;
}
}
}
}
运行结果如下:
你会发现售出了0、-1、-2号票,这肯定不符合实际情况,而且这种多线程异常的出现是很随机的,存在很大的隐蔽性,很可能你调试了千遍万遍也未必能遇到,结果投入使用以后出现啦,下面我们来分析一下这个异常是怎么出现: 线程是被CPU轮流执行的,执行的随机性很大,所以完全有可能当i减至1的时候一条线程执行到①处以后被另一条线程抢走了资源,后来的线程完成了i--操作以后,上条线程又获得资源继续执行,结果打印出来的就是0,因为i是被多条线程共享的嘛,如果是多条线程同时在①处倒下了,就是出现-1,-2了。
2、如何解决上面的安全问题呢,有这么一个思路:让每个线程都能顺利执行完与共享数据相关的操作代码而不会被其他线程所打断(即个线程在此代码块的执行上互斥),以保持共享数据的一致性,这个时候就用到了同步。把上面的Ticket改成如下形式即可
class Ticket implements Runnable{
private int i = 100;
Object obj = new Object();
public void run(){
while(true){
//obj就是同步代码块所持有的同步监视器,类似于一把锁,每次只能有一个线程可以获取钥
//匙打开这把锁执行里面的代码块,执行完后再释放钥匙让其他线程抢
synchronized(obj){ //synchronized+(监视器)即可让后面的{}变成线程互斥的
if(i>0){ //①
System.out.println(Thread.currentThread().getName()+"售出"+i--+"号票");
}else{
break;
}
}
}
}
}
使用同步的原则:必须要有多个线程,多个线程进行了改变共享资源的操作,多个线程持有的要是同一把锁。
使用同步的弊端:每个线程在执行同步代码块之前都要判断锁,影响了性能。
同步函数:非静态同步函数的锁是this,静态同步函数的锁是本类的字节码文件对象(类名.class),它们的使用方式是直接在函数返回值类型修饰符前加关键字synchronized即可,不需要传入锁对象。
3、死锁:当两个线程互相等待对方释放锁就会导致程序无法继续向下执行,这就是死锁,一般出现在嵌套同步中,在开发中一定
要注意不要出现先死锁。下面通过一段代码来说明
class LockClass {
public static final Object A = new Object();
public static final Object B = new Object();
}
public class DeathLock implements Runnable{
private boolean flag;
public DeathLock(boolean flag){
this.flag = flag;
}
public void run() {
if(flag){
while(true){
synchronized(LockClass.A){
System.out.println(Thread.currentThread().getName()+
"--获得锁A,等待锁B");
synchronized(LockClass.B){
System.out.println(Thread.currentThread().getName()+
"--获得锁B");
}
}
}
}else{
while(true){
synchronized(LockClass.B){
System.out.println(Thread.currentThread().getName()+
"--获得锁B,等待锁A");
synchronized(LockClass.A){
System.out.println(Thread.currentThread().getName()+
"--获得锁A");
}
}
}
}
}
public static void main(String[] args)
{
new Thread(new DeathLock(true),"线程一").start();
new Thread(new DeathLock(false),"线程二").start();
}
}
这段代码的执行结果为:
线程一与线程二互相等待对方所需要的锁,而释放这些锁的条件又都依赖于对方当前所持有的锁,就造成了死锁。
四、一、Lock
Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的Condition对象。锁是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问。一次只能有一个线程获得锁,对共享资源的所有访问都需要首先获得锁。不过,某些锁可能允许对共享资源并发访问,如ReadWriteLock的读取锁。synchronized方法或语句的使用提供了对与每个对象相关的隐式监视器锁的访问,但却强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。虽然synchronized方法和语句的范围机制使得使用监视器锁编程方便了很多,而且还帮助避免了很多涉及到锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。Lock作为一种更为灵活的锁,其在使用上跟synchronized很相似,对上面的示例稍加改动就可以实现同样的功能:
class Ticket implements Runnable{
private int i = 100;
Lock lock = new ReentrantLock();
Object obj = new Object();
public void run(){
while(true){
//只需要对synchronized的简单替换即可
lock.lock(); //获取锁
try{
if(i>0){
System.out.println(Thread.currentThread().getName()+
"售出"+i--+"号票");
}else{
break;
}
}finally{
lock.unlock(); //释放锁
}
}
}
}