多线程概述、同步死锁、线程控制、并发访问
一、多线程的概述
1.进程:正在运行的程序,是系统进行资源分配和调用的独立单位。
1.对于"操作系统"而言,每个独立运行的程序就是独立的"进程";
2."操作系统"会分别针对每个"进程"分配一块独立的内存空间;
2.多进程:
1)."操作系统"可以同时维护多个"应用程序"的执行。
2).每个应用程序都交由操作系统管理,在某个时间点上,会有一个应用程序被操作系统分配给CPU去执行,执行一会后,会被操作系统
终止执行,并交由另一个"应用程序"继续执行。由于转换非常快,CPU的运算速度也非常快,这就让我们感觉好像是多个应用程序在
同时执行一样。
3).多进程的意义:
1).方便了我们用户的使用。我们可以同时启动多个程序,一边听歌,一边上网,一边下载;
2).充分的利用CPU资源;
3.线程:
1.线程是由一个"进程"的内部启动的,可以脱离于"主进程"而独立运行的一块代码;
2.一个线程一旦启动,将和"主进程"并行运行,一起面对操作系统,抢占系统资源;
3.一个"进程"可以启动多个"线程";
4.多线程:
1).一个进程可以同时启动多个线程去单独运行;这个程序就是一个多线程程序;
2).多线程的意义:
1).可以使我们的应用程序"同时"运行多个非常复杂的代码;
3).充分的利用了CPU的资源;
5.并行和并发:
1)."并行"是指逻辑上一起在执行,它强调的是在"同一段时间内"一起运行的程序
2)."并发"是指物理上的抢占同一资源。它强调的是在"同一时刻时"一起抢占系统的某个共享资源
6.JAVA程序运行原理
java 命令会启动 java 虚拟机,启动 JVM,等于启动了一个应用程序,也就是启动了一个进程。
该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。所以 main方法运行在主线程中。
在此之前的所有程序都是单线程的。
★★★
JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的。
二、多线程实现方式:
1.继承Thread类;
A:实现步骤:
1.自定义一个线程类,要继承自Thread;
2.重写Thread中的run()方法;将要在线程中执行的代码写到这里;
3.启动线程:
1).实例化一个我们自定义的线程类;
2).调用它的start()方法;启动线程;
B:注意:
1.一个线程类,可以实例化多个对象,并分别启动。也就意味着多个线程同时运行;
2.一个线程对象,只能调用一次start,不能重复调用。否则抛出异常:java.lang.IllegalThreadStateException
★★
3.只用调用start()才是启动线程。run()方法内的代码会以一个线程的方式去运行;
class Thread{
public void start(){
run();//调用的是子类的run()方法;
}
public void run(){
}
}
class MyThread extends Thread{
//重写父类的run
public void run(){
System.out.println("子类的run");
}
}
main(){
MyThread t1 = new MyThread();
t1.start();
}
2.实现Runnable接口;
三、设置获取线程的名称
1.即使我们不去设定线程名称,每个线程都会有一个默认的线程名称;
2.获取线程名:
public final String getName();
默认的线程名称:Thread - "索引值(按启动顺序,从0开始)"
public static Thread currentThread()——返回对当前正在执行的线程对象的引用。————这样就可以获取任意方法所在的线程名称
3.设置线程名称:
public final void setName(String name):设置线程的名称
public Thread( String name):分配新的 Thread 对象。这种构造方法与 Thread(null, null, name) 具有相同的作用。 参数:name - 新线程的名称。
四、线程的调度
1.线程有两种调度模型:
分时调度模型:
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
抢占式调度模型:
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,
优先级高的线程获取的 CPU 时间片相对多一些。
Java使用的是抢占式调度模型
2.设置获取线程的优先级:
Java的线程的优先级范围:1--10(从低到高)
设置优先级:setPriority(int n):n一定要在1--10之间,否则抛异常;
获取优先级:getPriority():
3.注意
1.Java中的优先级跟操作系统的优先级往往不匹配,不要依赖于优先级,
去试图让某些线程先执行完毕,因为这得不到保证;
2.如果线程的内部工作非常简单,那么设置优先级的效果将不会明显;
五、线程的控制
1、休眠:
public static void sleep(long millis):休眠到指定的m毫秒后,将自动醒来;并且处于就粗状态。
由于其是静态的那么使用类名就可以调用。写在哪个线程内部,就是让哪个线程休眠。
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
//让当前的线程休眠多长时间;
Thread.sleep(1000 * 1);
} catch (InterruptedException e) {
}
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
String str = sdf.format(date);
System.out.println(str);
}//让进程休眠一秒钟,在执行
}
}
2、加入:
public final void join();意味着,调用者可以获得优先执行,调用者执行完之后,才能执行其他的进程。
3、礼让:
public static void yield():退回到"就绪状态",可能会被操作系统重新分配;
1.直接使用类名就可以调用;
2.调用的线程,会退回到就绪的状态,同其它线程站在同一起跑线上,等待着被操作系统分配资源;
礼让的线程,很有可能被再次分配到执行权;
4、守护:
public final void setDaemon(boolean on):on为true,守护线程。
主进程结束,守护线程也结束,但不会立即结束,会有一个小缓冲;
在主程序中,将某个线程设置为守护线程。
5、中断:stop()(过时了)
public void interrupt():线程一定要处于:Object-->wait();Thread-->sleep();Thread--join()三种状态之一时,才能触发此异常;
只有当线程处于以下三种状态的阻塞时,才会有效:
1.Object-->wait()
2.Thread-->sleep();
3.Thread-->join();
当调用interrupt()方法时,会促使虚拟机产生一个InterruptedException异常
并且被线程内的上述三个方法调用的catch捕获,捕获到后,可以结束线程的执行;
6、以上五种线程控制的方法,两种静态方法,sleep()和yield()都是使用类名调用,常在线程内部使用。
7.面试题
A:当一个线程被start()后会被立即执行吗?
不会的,当start()后会进入“就绪”状态,由操作系统分配执行权力。只有操作系统分配了执行权,才会获得执行。
B:但一个线程sleep醒来后,会不会就执行,只是就绪状态。
六、实现线程的的第二种方法--实现Runnable接口
1.步骤
自定义一个类,实现Runnable接口
重写接口中的run()方法
启动线程:
实例化自定义类对象
实例化Thread对象,并将我们自定义对象当做参数传递给Thread的构造方法
2.获取线程名称
public static Thread currentThread()——返回对当前正在执行的线程对象的引用。————
这样就可以获取任意方法所在的线程名称调用Thread类的start()方法启动线程。
实现方法的原码抽象分析:
class Thread{
* private Runnable runnable = null;
* Thread(Runnable run){
* this.runnable = run;
* }
* void start(){
* if(this.runnable == null){
* run();//如果不是用Runnable对象构造的,就执行我们的类的run,这个自定义类就是应该是Thread的子类;
* }else{
* this.runnable.run();//执行的是我们实现Runnable接口的类中的run()
* }
* }
* void run(){
* }
* }
* class MyThread extends Thread{
* //重写父类的run
* void run(){
* }
* }
* main(){
* new MyThread().start();
* }
3.两种实现形式的比较:
第一种需要继承自一个类,对子类形成一种限制,如果子类已经继承了其他类,就不能再继承这个类了。
第二种方式比较灵活;推荐使用。
适合多个相同的代码的程序去处理同一个资源的情况,把线程同程序的代码和数据有效分离了,较好的体现了面向对象的设计思想。
4.购买电影票的实例分析:
票池是一个共享的数据区域,所以只能实例化一个对象,否则就不对了。
票池的get方法,要是多个线程并行访问,会长生并发性问题,即产生重复的票和负票。
解决方法:对可能产生并发访问的方法加锁。
七、解决并发性问题:
1.使用关键字:synchronized
2.使用格式:ynchronized(被锁的对象){可能被并发访问的代码}。
被锁的对象:是一个对象的引用,表示"被锁的对象",意味着,如果一个线程正在访问这个"被锁对象"的synchronized
代码块时,其它线程不能访问这个对象中的任何的synchronized的代码块或synchronized的方法;
3.synchronized可以修饰一个代码块:同步代码块
synchronized可以修饰一个方法:同步方法
★★4.静态的方法可以被synchronized修饰。
静态的方法内部也可以包含synchronized的代码块,但一般锁定是某个类的class对象。
可以将一个静态方法声明为同步的.在静态方法内添加同步代码块,一般锁的是某个类的class对象。
5.同步的特点好处和弊端:
1).特点:当一个代码块被同步后,当一个线程访问时,其它线程列队等待。它能保证同一时刻只为一个线程服务,一个线程执行完毕,才能轮到下一个线程执行;
2).好处:可以解决多线程访问的并发性问题;
3).弊端:因为要使其它线程列队等待,所以会有其它额外的操作,而且这些操作都非常耗时,所以效率比较低;
八、以前的线程问题
StringBuffer:线程安全的。效率低
StringBuilder:线程不安全的。效率高
集合:
ArrayList:线程不安全。效率高
Vector:线程安全,效率低
Hashtable:线程安全的,效率低;
HashMap:线程不安全,效率高
工具:
Collections类中,有些:synchronizedXxxx():方法,可以将线程不安全的集合转换为线程安全的集合;
ArrayList
strList = new ArrayList<>();//线程不安全的
strList.add("aaa");
strList.add("bbb");
//转换为线程安全的List
List list2 = Collections.synchronizedList(strList);