多线程
1、进程:一个正在执行的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。
线程:进程中的一个独立的控制单元,线程在控制着进程的执行。
创建线程的方式:
1)继承Thread类:
①定义类继承Thread类;
②复写Thread类的run方法;
目的:将自定义的代码存储在run方法中,让线程运行(启动方式:被start方法调用)。
③调用线程的start方法:该方法有两个作用:启动线程,
多个线程运行结果每一次都不相同,是因为多个线程都获取CPU的执行权,CPU执行到谁,谁就运行,但要明确一点,在某一个时刻,只能有一个程序在运行(多核除外)。只不过CPU在做着快速的切换,以达到看上去是同时运行的效果。我们可以形象的把多线程的运行行为看做是在互相抢夺CPU的执行权。
这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多久,CPU说了算。
为什么要覆盖run方法?
因为Thread类用于描述线程,所以该类就定义了一个功能,用于存储线程要运行的代码。该存储功能就是run方法。也就是说,Thread类中的run方法是用于存储线程要运行的代码。main方法是jvn调用的(主线程),而run方法里的代码是主线程调用的。
start方法:开启线程,并执行该线程的run方法;
run方法:仅仅是对象调用方法。而线程创建了,如果不调用start,线程并没有执行。
虽然start也是启动线程并调用run方法,但不能将start换成run,如果换成run,则只能被当做一般方法一样,被主线程调用。代码还是按顺序执行。
线程的五个状态:被创建、运行、冻结、阻塞、消亡。
运行:具备执行资格,且具有CPU执行权;
阻塞:具备执行资格,但未获得CPU执行权;
冻结:分两种:sleep(int time),wait;sleep是时间一到自动唤醒,wait冻结之后,自己无法醒来,需要被调用notify被唤醒。这个状态的线程不具备执行资格。
消亡:分两种:被消亡(stop()),正常运行结束。
static Thread Thread.currentThread()方法:是获取当前正在运行的线程对象。
getName():获取线程名称。
这种方式创建一个线程,只能start一次!
2)实现Runnable接口:
①定义类实现Runnable接口;
②覆盖Runnable接口中的run方法;
将线程要运行的代码存放在该run方法中。
③通过Thread类建立线程对象;
④将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数;
为什么要将Runnable接口的子类对象传递给Thread的构造函数?
因为,自定义的run方法所属的对象是Runnable接口的子类对象,所以,要让线程去执行指定对象的run方法,就必须明确该run方法所属的对象。
⑤调用Thread类中的start方法开启线程并调用Runnable接口子类的run方法。
示例:
class Ticket implements Runnable
{
private int tickets = 100;
public void run()
{
while(true) {
if(tickets > 0) { //这个地方有安全隐患:如果多个线程同时判断完tickets=1时,某个线程打印1号票后,其他线程打印的就是非法值了。
System.out.println(Thread.currentThread().getName() + “…sales:” + tickets--);
}
}
}
}
class TicketsDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
}
}
那么,继承方式和实现方式有什么区别?
实现方式的好处:避免了单继承(java只支持单继承)的局限性,而且资源也能独立出来共享;在定义线程时,建议使用实现方式。
继承Thread:线程代码存放在Thread子类的run方法中;
实现Runnable:线程代码存在接口的子类的run方法中。
通过上面的例子运行分析发现,有可能会打印出0,-1,-2等错票(非法值)
这是因为,多线程的运行安全出现了安全问题。
问题的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行,导致共享数据的错误。
解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
java对于多线程的安全问题提供的解决方式————【同步代码块】。
synchronized(对象)
{
需要被同步的代码;
}
如果整个函数的代码都需要同步,则也可给函数用synchronized修饰:
public synchronized void add(int n) {…}
同步函数的锁对象是this。所以如果同步函数跟另一个代码块要使用同一把锁,该代码块也要使用this这个对象。
如果同步函数被static修饰后,使用的锁是什么呢?
首先不是this,因为静态方法中不可以定义this,静态进内存时,内存中还没有本类对象,但一定有该类对应的字节码文件对象:类名.class
机制:互斥锁。同时只有一个线程拿到锁,拿到锁才能执行该代码块,当执行完再把锁释放。
类似于【火车上的卫生间】
同步的前提:
1)必须要有两个或者两个以上的线程;
2)必须是多个线程使用同一把锁;
必须保证同步中只能有一个线程在运行。
好处:解决了多线程的安全问题;
弊端:多个线程需要判断锁,较为消耗资源。
单例设计模式中的懒汉式:
为了保障安全,并且加上synchronized之后效率不是很低,写法应为:
class Single
{
private static Single instance = null;
private Single() {}
public static Single getInstance()
{
if(instance == null) //加两次null判断
{
synchronized(Single.class) //不是每次都是加锁之后才判断(这样每个线程执行到这都要等待),而是先判断(如果不为空则不用等待),再加锁
{
if(instance == null)
instance = new Single();
}
}
return instance;
}
}
面试常遇到:懒汉式的特点:实例的延迟加载。不加同步则可能出现安全问题。但加了同步(同步代码块/同步方法)则稍微有些低效。可以用上面的双重判断的方式解决效率问题。加同步使用的锁是:该类所属的字节码文件对象。
面试常遇到:死锁
拿到A锁的线程1等待获取B锁,而拿到B锁的线程2又在等待获取A锁。两者相持不下,就会产生死锁。
等待唤醒机制;
wait,notify(),notifyAll()这些方法都是用在同步里,因为要对持有对象监视器(锁)的线程进行操作。所以要使用在同步中,因为只有同步才具有锁。
为什么这些操作线程的方法要定义在Object类中呢?
因为这些方法在操作同步中线程时,都必须要标识他们所操作线程持有的锁,只有同一个锁上的被等待线程,可以被同一个锁上的其他线程notify。不可以对不同锁中的线程进行唤醒。
也就是说,等待和唤醒必须是同一个锁。
而锁可以是任意对象,所以,可以被任意对象调用的方法,要定义在Object类中。
jdk1.5的新用法:
Lock:代替了synchronized
Condition:代替了wait、notify、notifyAll
这样,一个Lock可以对应多个Condition对象,可以实现只唤醒对方的线程。
如何停止线程?
只有一种,run方法结束。开启多线程运行,代码通常是循环结构,只要控制住循环,就可以让run方法结束。也就是线程结束。但有一个特殊情况,就是当线程处于冻结状态,则线程不会读取到标记,因此不会结束。如果没有指定的方式让冻结的线程恢复到运行状态,这时需要对冻结状态进行清除,强制让线程恢复到运行状态,这样才可以进行操作让他结束。
Thread类提供的方法是:interrupt()
public final void setDaemon(boolean on):
将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。on如果为 true,则将该线程标记为守护线程。
public final void join()
当线程A执行到了B线程的join方法时,A就会等待,等B线程执行完,A才会执行。
join可以用来临时加入线程执行。