1.进程和线程
进程:正在执行中的程序;每一个进程执行都有一个执行顺序,该顺序是一个执行路径,又叫控制单元;线程:线程是进程中的内容,进程中的一个独立的控制单元,线程控制着进程的执行;Java虚拟机允许应用程序并发的运行多个执行线程;当引进线程后,CPU在各个线程之间作着快速的切换,单核CPU在某一时刻只有一个线程在运行,多个线程在宏观上同时执行;多线程与多进程:(1).一个进程中至少有一个线程;(2).与进程相比较,线程更“轻量级”,开销小;(3).每个进程拥有自己的一整套变量,而线程则共享数据,共享数据使得线程之间的通信相比进程之间的通信更有效率,更容易,但是同时也会出现共享数据的安全问题;主线程:JVM在编译时期会生成一个javac.exe的进程,在运行时期,会生成一个java.exe的进程;执行期间java.exe进程中至少有一个线程在负责Java程序的执行,而且这个线程运行的代码存在于Java程序的main()方法中,该线程称之为主线程;JVM在启动时并不只是只执行一个主线程那么简单,也就是说并非是单线程,而是多线程的执行任务,最明显的就是在程序运行时期还会有专门负责垃圾回收的线程在执行;多线程执行的意义所在:(1).在宏观上产生一个同时运行的效果;(2).提高执行效率;
2.线程的状态
线程的状态:(1).BLOCKED:受阻塞的,正在等待监视器锁的线程;(2).NEW:创建后尚未启动的线程;(3).RUNNABLE:可运行的线程;(4).WAITING:等待线程;(5).TIMED_WAITING:具有指定等待时间的等待线程;(6).TERMINATED:终止的线程;
线程的状态示意图:
3.线程的创建
3.1 继承方式
Java提供的类库中提供了对线程这类事物的描述:封装为Thread类;创建线程的继承方式:将自定义的类声明为Thread类的子类,重写run()方法;具体步骤:(1).定义类继承Thread类;
Thread类中定义了一些用来获取和操作线程对象的方法:获取当前正在执行的线程对象的引用:static Thread currentThread();获取线程的名称:String getName();默认名称:Thread-编号(从0开始);获取线程的优先级:int getPriority();获取线程的状态:Thread.State getState();设置线程名称:void setName(String name)或者在构造时调用父类的构造方法;设置线程优先级:void setPriority(int newPriority);获取线程信息:String toString();包括线程的名称,优先级,线程组;
(2).覆盖重写Thread类中的run()方法:public void run(){}
覆盖run()方法:将自定义的代码存储在run方法中,供线程运行;
1).Thread类用于描述线程,该类定义了一个功能,用于存储线程要运行的代码,该存储功能就是run方法;run方法相当于线程执行代码的一个存储空间;
2).主线程的执行代码存放在main方法中,自定义线程的执行代码存放在自定义线程的run方法中;
3).Thread类中的run方法没有定义有意义的内容,父类提供了功能定义,子类只需覆盖,添加自己的内容即可;
(3).创建自定义类的对象,即创建线程;
(4).调用类中从Thread继承来的start()方法:启动线程;执行run方法;
注意:
对象.run():仅仅就是普通的对象调用方法;线程创建了,但是没有启动运行;
对象.start():开启线程并执行线程中的run()方法;
(5).对上述线程执行过程的分析:
程序执行后,有两个线程在运行(实际上不止两个),一个是主线程,一个是自己创建的线程;
多个线程都需要获取CPU执行权,但是在某一时刻,只能有一个线程在运行(多核CPU除外),CPU在各个线程之间做着频繁的切换,从宏观上是在同时运行;
通过运行结果的不确定性体现了多线程运行时的特性:随机性;
3.2 实现方式
通过对JavaAPI的分析,发现Thread也是实现了上层的一个接口:Runnable接口;
另外一种创建线程的方式就是实现Runnable接口;
具体步骤:
(1).自定义类实现Runnable接口;
(2).覆盖Runnable接口中的run()方法;
将自定义线程需要执行的代码存放在run方法中;
(3).通过Thread类建立线程对象;
(4).将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法;
自定义的run方法所属的对象是Runnable接口的子类对象,要让new线程执行指定对象的run方法,就在线程创建时明确该run方法所属的对象;
(5).调用Thread类的start方法启动线程,并调用Runnable接口子类的run方法;
运行结果:
3.3 实现方式和继承方式总结
实现方式:建议使用;避免了单继承的局限性;线程代码存放在Runnable接口子类的run方法中;
继承方式:当继承Thread类后,就不能在继承其他类;线程代码存放在Thread子类的run方法中;
4.多线程中的安全问题
(1).问题产生的原因
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,并没有执行完,而这时另一个线程也参与进来开始执行这部分代码,导致共享数据的错误;
(2).解决办法
对多条操作共享数据的语句在执行时,只能让一个线程执行完,在执行过程中,不允许其他线程参与执行;
(3).同步代码块
Java中对于多线程的安全问题提供的专业的解决方法;
格式:
synchronized(锁对象) //持有锁的线程可以在同步中执行;
{
需要被同步的代码块(操作共享数据);
}
如果某个线程的整个run方法被同步了就变成了单线程;
没有持有锁的线程即使获得了CPU执行权,也不能运行,还是要等待;
(4).同步必须要满足的两个前提
必须要有两个和两个以上的线程;
必须是多个线程使用同一个锁;
(5).同步的利弊
利:解决多线程的安全问题;
弊:多个线程要判断锁,消耗系统资源;
(6).查找安全问题
1).明确哪些代码是多线程运行代码;
2).明确共享数据;
3).明确多线程运行代码中哪些语句是操作共享数据的;
(7).同步函数
当整个方法中的代码都需要被同步时,可以将该方法定义成同步函数,使用关键字synchronized修饰,关键字放在返回值类型前面;
同步函数的示例:
同步函数的锁:函数需要被对象调用,函数都有一个所属的对象引用this,所以同步函数的锁是this;
验证同步函数的锁是this:
静态同步函数:静态方法加载进内存时,没有本类对象,但是有该类对应的字节码文件对象:类名.class;该对象的类型是Class,用这个字节码文件对象作为静态同步函数的锁;
(8).死锁问题
死锁的特征:线程间同步中嵌套同步,持有的是不一样的锁,分别请求持有对方的锁;
编写一个死锁程序:
5.线程间的通信
多个线程操作同一资源,但是操作的动作不同;(1).等待唤醒机制
wait():释放资源,释放锁;sleep():释放资源,不释放锁;notify():唤醒某个等待的进程;notifyAll():唤醒所有等待的进程;wait,notify,notifyAll都是用在同步中:因为要对持有监视器(锁)的线程进行操作,只有同步才具有锁;这些操作线程的方法都定义在Object类中:因为这些方法在操作同步中的线程时,必须标识它们所操作的线程所持有的锁,否则会出现非法的监视器状态异常;锁即对象,所以定义在Object类最合适;同一个锁上被wait的线程可以被同一锁的notify唤醒;等待和唤醒必须是同一个锁;用静态修饰的变量也是共享数据,但是不建议声明静态,生存周期太长;(2).生产者和消费者问题
运行结果:
现在就生产者消费问题中的一些情况进行分析:
情况一:当分别只有一个生产者线程和一个消费者线程时,赋值动作和打印动作在同时操作共享数据,导致输出结果混乱;
解决办法:将操作共享数据的代码同步;
情况二:当分别只有一个生产者和消费者线程时,出现连续生产多次或者连续消费多次;
解决方法:添加标记,判断条件,同时引进等待唤醒机制;
情况三:当单个生产者和单个消费者之间操作资源成功后,考虑引进多个生产者和多个消费者;但是在多个线程的情况下,还是会出现和情况二一样的问题;
解决方法:循环判断标记;
定义while()循环判断标记:让被唤醒的线程再一次判断标记;
情况四:多个生产者和多个消费者,循环判断标记,会出现多个线程都死等的状态;
解决办法:notify唤醒只唤醒某一个线程,使用notifyAll唤醒所有线程;
定义notifyAll(),需要唤醒对方线程,使用notify容易出现只唤醒本方线程的情况,导致程序中所有线程都等待;
(3).JDK5.0出现的显式的锁机制
JavaAPI中包java.util.concurrent.locks为锁和等待条件提供一个框架的接口和类;
Java.util.concurrent.locks
|--interface:Condition;
将Object监视器方法(wait(),notify(),notifyAll())分解成截然不同的对象,以便通过将这些对象与任意Lock实现组合使用,为每个对象提供多个等待;其中Lock替代了synchronized方法和语句的使用,Condition替代了Object监视器方法的使用;
await(),signal(),signalAll();
|--interface:Lock
|--已知实现类:ReentrantLock;
Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作;
void lock();--获取锁
Condition newCondition();--返回绑定到该锁实例的Condition实例
void unlock();--释放锁
6.其他线程属性
(1).停止线程
早期停止线程的方法是调用stop方法,但是该方法已经过时;
现在停止线程只有一种方式,就是run方法执行完毕,同时由于开启多线程运行,运行代码通常是循环结构,因此只要控制住循环,就可以让run方法结束,同时线程也会停止;
(2).interrupt()方法:中断线程状态
当线程处于冻结状态时,可能读取不到标记,线程就不会停止;
当没有指定的方式让冻结的线程恢复到运行状态时,这是需要对冻结状态进行清除;强制让线程恢复到运行状态中来,这样就可以操作标记让线程停止;
Thread类中提供的这种清除线程冻结状态的方法是interrupt();
(3).join方法:
当A线程执行到了B线程的join()方法时,A线程会等待,直到B线程执行结束后(在此期间还可以和已经启动的其他线程并发执行,而并不是所有线程都等B线程执行结束),A才会重新参与运行;
某个线程等待另一个线程去死;
(4).守护线程:
public final void setDaemon(boolean on)
必须在启动线程前调用;
守护线程可以理解为后台线程,当程序中所有线程都是守护线程时,程序结束;
当所有的前台线程结束,后台线程自动结束;
(5).yield方法:暂停当前正在执行的线程对象,并执行其他线程;static void yield();