谈这个之前首先得区分一下什么是线程什么是进程?
进程:正在运行中的程序。
线程:就是进程当中的执行单元或者执行情景或者执行路径。负责进程中程序执行的控制单元。 当一个进程当中,线程有多个时,就是多线程,我们可以把这个应用程序称之为多线程应用程序。 其实java程序就是一个多线程程序。在执行main函数的同时,垃圾回收器也在回收堆内存当中的垃圾, 所以执行main函数的线程和执行垃圾回收器的线程是同时进行的,这就是多线程。jvm在启动线程的时候,每个线程都有自己的执行内容,其中一个负责执行main函数的内容,那么这个线程我们就称之为主线程。负责垃圾回收器运行的线程我们称之为垃圾回收线程。
为什么要启动多线程呢?
因为有多部分的代码需要同时执行,而且每一个线程都有自己要执行的内容,这个内容我们称之为线程的任务。简单说:启动线程就是为了执行任务,当任务有多个需要同时执行的时候,就是多线程。
如何去创建线程呢?
java既然需要调用底层来实现进程的建立和线程的创建,那么java应该有对外提供一个 描述线程的对象的类,来方便程序员对线程的操作。 java的java.lang包当中,去找线程的核心类Thread。
通过查阅thread描述可以发现:
创建线程有两种方式:
方式一: 继承thread类,然后重写run()方法。
步骤:
1.定义类,继承thread类。
2.重写thread类当中的run方法。
3.创建thread类的子类对象,也就是创建了线程对象。
4.调用线程对象的start方法开启线程。
调用run方法没有执行新创建的线程,还是一个线程在执行,主线程创建了
两个线程对象之后,主线程指向student中的run方法,其实在做操作的一值是主线程。
线程只创建不行,还需要运行才会出现多条执行路径。
运行线程必须要使用start方法。
start方法有两个作用:
1.开启了线程,让线程可以运行。
2.让jvm虚拟机调用了run方法。
调用run方法和调用start方法的区别?
调用run方法,仅仅是一般对象在调用对象当中的方法,并没有开启线程,
还是由主线程来执行run方法的内容的。
调用start方法就开启了新的线程(有了一条新的执行路径),
这个线程就去执行run方法。
多线程的运行状态:
1.新建状态(New):
当用new操作符创建一个线程时, 例如new Thread(r),线程还没有开始运行,
此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码.
2.就绪状态(Runnable)
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。
当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,
并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。
处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争
CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时
运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就
绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread
scheduler)来调度的。
3.运行状态(Running)
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.
4. 阻塞状态(Blocked)
线程运行过程中,可能由于各种原因进入阻塞状态:
1>线程通过调用sleep方法进入睡眠状态;
2>线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会
返回到它的调用者;
3>线程试图得到一个锁,而该锁正被其他线程持有;
4>线程在等待某个触发条件;
所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态
的线程就可以获得CPU时间,进入运行状态。
5. 死亡状态(Dead)
有两个原因会导致线程死亡:
1) run方法正常退出而自然死亡,
2) 一个未捕获的异常终止了run方法而使线程猝死。
为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),
需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是
new状态且不是可运行的, 或者线程死亡了,则返回false.
线程安全问题:
安全问题产生的原因:
* 1.多个线程在操作共享数据。
* 2.操作共享数据的代码有多条.
*
* 分析线程是否会有安全问题呢?
* 依据:线程任务当中有没有共享数据,该数据是否被多条语句操作。
*
* 产生安全问题的解决方案:
* 只要保证一个线程在执行多条操作共享数据的语句时,其他线程不能参与运算即可。
* 当该线程执行完之后,其他线程再来执行这个语句。
*
* 代码体现:
* java给我们提供了具体的代码:就是同步代码块
* 格式:synchronized(对象){ //这个对象可以是任意的对象
* //需要被同步执行的语句
* }
*
* 同步的原理:其实就是将需要同步的代码进行了封装,然后再封装的代码块之上加了一把锁。
*
* 同步的好处:解决了多线程的安全问题。
* 同步的弊端:会降低性能。
*
* 一种现象,出现了多线程安全性问题,为了解决,加了同步,发现问题依旧,
* 产生这个现象的原因是什么呢?
*
*同步的前提:
*1.必须要保证同步中有多个线程,因为同步中只有一个线程这个同步是没有任何意义的。
*2.必须要保证多线程在同步中使用的是同一把锁。
*
*必须要保证多线程的锁是同一个,这样才能使多线程同步。
怎么让函数具备同步性呢?
直接用同步关键字作为修饰符,修饰函数即可,这时函数就具备了同步性。
这个函数就是同步函数,他是同步的另一种体现形式。
这种形式写法简单。
通过两个线程来验证一下同步函数的锁是什么?
这两个线程都是在卖票,一个在同步代码块中卖票,一个在同步函数中卖票。
如果两个线程用到是同一把锁,就会同步,就不会出现卖错票的情况。
同步函数使用的锁是什么呢?
函数被对像调用,代表调用函数对象的引用就是this.
那也就是说同步函数使用的锁就是this。
同步代码块和同步函数之间有什么区别呢?
1.同步函数比同步代码块写法简单。
2.同步函数使用的锁是this,同步代码块使用的锁是任意的对象。
建议开发时使用同步代码块,因为锁可以是任意的,尤其是我们需要不同的锁的时候,
同步代码块才能解决这个问题。
静态同步函数的锁是什么?
静态随着类的加载而加载,而这时内存中存储的对象至少有一个,也就是该类的字节码文件对象。
这个对象的表现形式:类名.class,他表示字节码文件对象
[关于单例模式的分析]
//饿汉式
class Single{
private Single(){}
private static final Single s = new Single();
public static Single getInstance(){
return s;
}
}
/**如果饿汉式在多线程当中会有线程不安全么?
* 答:不会,因为操作共享数据的语句只有一条,所以不会导致线程不安全现象。
*/
//懒汉式:单例模式的延迟加载
class Single2{
private static Single2 s = null;
private Single2(){}
public static Single2 getInstance(){
if (s==null) {
synchronized (Single2.class) {
if (s==null) {
s = new Single2();
}
}
}
return s;
}
}
/**分析懒汉式线程不安全原因:
* 有共享数据s,然后共享数据操作语句有多条,判断,赋值
* 怎么解决这个问题呢?
* 上锁。
* 懒汉式在多线程处理时会出现线程不安全的问题,为了解决加了同步,
* 虽然安全问题就解决了,但是性能降低了。
*
* 有没有办法在安全问题解决的同时又能提高性能呢?
* 方法是有的,使用同步代码块形式,
* 通过双重判断来完成这个过程。
* */
同步的另一个弊端:死锁
最常见的死锁的情况:
同步嵌套,同步中还有同步,然后两个同步用的不是一把锁。
尽量避免同步嵌套的情况
死锁的四个必要条件
1.互斥条件:资源不能被共享,只能由一个线程来使用。
2.请求与保持条件:已经得到了资源的进程,还可以申请新的资源。
3.非剥夺条件:已经分配的资源不能在相应的线程中前行的剥夺。
4.循环等待条件:系统中若干个线程组成了环路,该环路中每一个线程都在等待着相邻的线程占据的资源。
[线程间通信]
等待唤醒机制:
涉及到3个方法:
wait():等待,将正在执行的线程释放了其执行资格和执行权力,并且将它存储到线程池中。
(何为线程池:运行当中会出现很多正在被冻结的线程,都会储存到线程池当中,线程池其实就是容器,
用来存储线程对象的,暂时不用的线程也要留着)。
notify():唤醒,唤醒了线程池中被wait了的线程,注意一次只能唤醒一个,而且唤醒的线程
对象时任意的。
notifyAll():唤醒全部,他可以将线程池中所有被wait的线程都唤醒。
唤醒就是让线程池中的线程具备执行资格。
这些方法只有使用在同步中才能有效。
这些方法在使用时必需要表明所属的锁,这样我们才可以明确方法操作的到底是哪个锁上的线程。
为什么这些操作线程的方法要定义在object类当中。
因为这些方法在使用时必须要表明所属的锁,而锁可以是任意对象,能被任意对象调用的方法
一定定义在object类当中。
等待唤醒机制的优化:代码的重新整理
代码的优化:
把同步的代码的操作内容定义在资源中,比较好,为什么呢?
因为更符合面向对象的思想。
把类中的方法设置为synchronized修饰符修饰,那么他的对象就是线程安全的对象。