1.多线程的概念以及2种启动方式
进程(Process):每个独立执行的程序(正在执行的程序)(不一定有界面)
线程(Thread)
:是一个程序内部的一条执行路径,java虚拟机允许应用程序并发地运行多个执行路径(相当于
进程中的一个子程序
)。线程是CPU调度和分派的基本单位
线程与进程的区别:
(1)每个进程都有独立的代码和数据空间(进程上下文),进程间的切换开销大;
同一进程内的线程共享代码和数据空间,线程切换的开销小。
(2)操作系统是以进程为单位的,而进程是以线程为单位的。
(3)多进程:在操作系统中能同时运行多个任务(程序);
多线程:在同一应用程序中多条执行路径同时执行。(即多个子程序)是实现并发机制的一种有效手段
(4)同样作为基本的执行单元,线程是划分得比进程更小的执行单位
(5)每个进程都有一段专用的内存区域。于此相反,线程却共享内存单元(包括代码和数据),通过共享的内存单元来实现数据交换、实时通信与必要的同步步骤。
**为什么需要线程???
因为多个不同任务可同时执行(“多线程的并发”)。---其实是高频度的轮流操作。
CPU的多线程的并发实际是每个线程轮流获得CPU的控制权(“CPU的时间片”)
单线程是一个任务一个任务的来执行,第一个任务执行完了才执行第二个任务。
**java线程是用户级别的(就是与CPU打交道),区别于CPU寄存器线程
**java main方法即是主线程,由它启动的若干线程即是子线程,默认地:主线程执行时间优于子线程
1.耗时的操作使用线程,提高应用程序响应
2.并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求。
3.多CPU系统中,使用线程提高CPU利用率
4.改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
创建新线程的两种方式
共有的特点:1.都要重写run()方法
2.都不能手动去调用run()方法,都是通过线程类的对象来调用start()方法。直接调用run()方法则仍然是单线程,没有启动多线程
(1)将类声明为Thread的子类并重写run()方法
class MyThread extends Thread{
public void run(){...}
}
创建此线程的实例并启动:
**
MyThread thread1 = new MyThread("线程1"); //
给每个线程设置自己的名字---给继承Thread类的子类写构造方法。如:
public MyThread(String threadName) {
super(threadName);
//调用父类的构造方法
}
thread1.start();
//启动线程--->自动运行其中的run方法(一般先执行主方法里的打印,再执行OtherClass类中的打印,因为main方法是主线程,OtherClass类是子线程)---主线程和子线程交替打印
** 或:使用内部类:new MyThread("线程1").start();
给主线程取名:
Thread.currentThread().setName("");
获取当前线程的名字:
Thread.currentThread().getName()
(2)定义实现Runnable接口的类
Runnable接口中只有一个方法public void run();用来定义线程运行体
class MyRun implements Runnable{
public void run(){...}
}
创建此线程的实例的时候,将这个类的实例作为参数传递到线程实例内部,然后再启动:
MyRun myRun = new MuRun();
Thread t1 = new Thread(myRun, "线程1");
t1.start();
Thread t2 = new Thread(myRun, "线程2");
//这里共用的myRunnable对象,所以就会共用RunnThread类中的成员变量(但是,
这种没有同步的,就可能出现数重复,或者多数
)
t2.start();
或:
Thread thread1 = new Thread(new MyRun());
thread1.start();
两种线程创建方式的比较:
使用Runnable接口:还可以从其他类继承;保持程序风格的一致性;可以为相同程序代码的多个线程提供共享的数据(当不存在共享数据时,这两种方法都可以)
直接继承Thread类:不能再从其他类继承;编写简单,可以直接操纵线程
(相当于把线程和任务结合在一起,不方便扩充。MyRun类相当于一个任务)
线程小结:
Java的线程是通过java.lang.Thread类来实现的。
当程序启动运行时,JVM会产生一个线程(主线程),主方法(main方法)就是在这个线程上运行的。
可以通过创建Thread的实例来创建新的线程。
*每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。
*通过调用Thread类的start()方法来启动一个线程。线程进入Runnable(可运行)状态,它将向线程调度器注册这个线程。
*调用start()方法并不一定马上会执行这个线程,正如上面所说,它只是进入Runnable 而不是Running。
注意,不要直接在程序中调用线程的run()方法。(直接调用不是启动线程,而是在本线程中执行方法)
2.多线程的调度以及优先级
1.线程的状态:(枚举类型的)
新建(NEW)
等待就绪(WAITING | TIMEWAITING)
运行(RUNNABLE)
阻塞(BLOCKED)
终止(TERMINATED)
2.促使线程在各种状态间转换的操作---线程的调度,比如Thread.sleep()是线程主动进入BLOCKED
(已经进入休眠状态的,不能在调度,会出异常)
3.学习应用几种"线程调度"方法
(1)join() throws InterruptedException 方法 ---“强行加入线程队列”在当前线程中调用另一个线程的 join()方法,则当前线程转入WAITING状态,直到另一个线程运行结束,当前线程再由阻塞转为就绪状态。谁把它启动起来,它就是阻塞的谁(阻塞它的宿主所在的线程)
阻塞的几种方法:
** 通过改变线程的优先级来阻塞(效果不好,一般不用)
Thread.currentThread().setPriority(
Thread.MAX_PRIORITY
);
设定线程的优先级并不能改变:主线程默认就获得CPU的优先权,那么我们就用更“强硬的阻塞主线程的方式”---join
** Thread对象.join(); ---谁调谁霸道,必须要运行完,别人才能运行,阻塞它的宿主所在的线程(即:在哪个线程中调用的join,就是阻塞的哪个线程)
** Thread对象.join(long 毫秒数); ---谁掉谁霸道,阻塞别人“毫秒",阻塞它的宿主所在的线程
** Thread对象.join(long 毫秒数,int 纳秒数);---
等待多少毫秒后,再阻塞多少毫秒
(2)Thread.sleep(long millis) throws InterruptedException 方法,“线程的自我休眠”,在休眠的时候(BLOCKED状态)。
(3) Thread.yield()方法---“线程礼让”,让出更多的CPU时间片控制权给其它更有价值的线程,减少本线程的占用时间
注意:该方法也是静态方法,调用该方法之后,只是使该线程暂停一下,不会阻塞该线程,而是使该线程直接进入就绪状态。则与该线程优先级相同或者高的线程有可能获得执行机会。
完全有一种可能,该线程暂停之后又立马获得运行机会。
(4)线程停止:如果线程的run方法中执行的是一个重复执行的循环,可以提供一个标记来控制循环是否执行
如果线程因为执行sleep()或wait()而进入了阻塞状态,此时要想停止它,可以使用interrupt()--“线程自我中断”,线程会抛出InterrupException异常
如果程序因为输入/输出的等待而阻塞,基本上必须等待输入/输出的动作完成才能离开阻塞状态。
无法用interrupt()方法来使得线程离开run()方法,要想离开,只能通过引发一个异常。
线程的非静态方法是通过自身调用而改变其它线程(宿主线程)的状态,静态方法是改变当前线程的状态
线程的优先级:优先级高的线程会获得较多的运行机会。
Java 线程的优先级用整数表示,取值范围是 1~10,Thread 类有以下三个静态常量:
static int
MAX_PRIORITY 线程可以具有的最高优先级,取值为 10。
static int
MIN_PRIORITY 线程可以具有的最低优先级,取值为 1。
static int
NORM_PRIORITY 分配给线程的默认优先级,取值为 5。
Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。
3.多线程的状态转换
线程的状态:要想实现多线程,必须在主线程中创建新的线程对象。任何线程有具有五种状态:创建、就绪、运行、阻塞、终止
新建状态(New): 新创建了一个线程对象。
就绪状态(Runnable): 线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。是一个过渡状态。
获取了CPU,执行程序代码时也属于就绪状态。
运行状态(Running): 执行run()。
阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
(分为主动阻塞、被动阻塞(别的线程要先运行,强制它阻塞,让其他线程运行完了才运行))
阻塞的情况分三种: 同步阻塞:若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。运行的线程在获取对象的同步锁时 。
等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。
当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
结束状态(Dead) :线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
4.多线程的同步问题---
synchronized
编程技巧 在方法中尽量少操作成员变量,多使用局部变量
关键字synchronized用来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该(加锁的)对象在任一时刻只能由一个线程访问。
一个方法使用关键字synchronized修饰后,当一个线程A使用这个方法时,其他线程想使用这个方法就必须等待,直到线程A使用完该方法。
操作的是同一个Runnable对象,所以为了保证共享资源的一致性,就要用同步
*线程安全--同步(synchronized)---不能同时进行
线程不考虑安全---异步(asynchronized) 即并发
互斥锁:
1.明白“对象互斥”的功能---确保同一时刻,只能有一个线程才能访问多个线程锁共享的资源
(1)
synchronized(互斥锁对象---往往用this){
// 需要同步的代码;(同步代码块---同步的粒度更细)
}
当一个对象正在调用这个的时候,其它对象就在这个外面等待
(2)synchronized(共享资源类.class){ //共享资源类的反射对象(反射--就是对象到类;实例化--就是类到对象)。只要是这个类的对象,都是同步的
//需要同步的代码块
}
同步类.class类有一种等价的写法:
synchronized放在静态方法前面(synchronized放在方法的返回值前即可)--不常用
(3) synchronized放在非静态方法上面(
synchronized放在方法的返回值前即可)
同步的互斥锁就是该方法所在的对象,这里等价于this
(4)
定义一把线程专用的"对象锁"--可以没有实际含义,它的作用只是为多线程需要同步的方法"加锁",减少内存的开销
//若是非静态---跟this的作用一样
private Object res1 = new Object();
//若是静态---跟同步类的作用一样
private static Object res1 = new Object();
实现方式:用面向业务的逻辑去编写线程,将线程和业务分开编写---解耦(耦合--如齿轮---紧密结合在一起的)
同步锁对象:
1.使用同步代码块时必须明确指明使用的同步锁。同步也可以使用字节码作为锁。int.class、String.class等均可以作为同步锁;
2.同步方法也可以用到的同步锁,此时的锁对象是this。
3.使用同步锁建议使用公共资源作为锁。换句话说就是,这段代码要修改哪个公共资源,就使用那个公共资源作为同步锁
5.多线程间的通信---生产者消费者模型
生产者生产满了就不能生产了,消费者消费完了就不能消费了,但是生产者在生产的过程中,消费者也可以消费。
需求:
1 同一时间内只能有一个生产者生产
2 同一时间内只能有一个消费者消费
3 生产者生产的同时消费者不能消费
4 消费者消费的同时生产者不能生产
消费者和生产者只能有一个在工作。
----同步方法或者同步代码快
5 共享空间空时消费者不能继续消费
6 共享空间满时生产者不能继续生产
这样就造成了死锁
wait-notify机制可以有效的解决“线程死锁(资源)”---这两个方法是所有对象(Object)的方法
注意:
1.Wait、notify、notifyAll方法的调用必须放在同步方法或同步代码块里面
2.是调用“同步锁”对象的wait、notify、notifyAll方法而不是线程对象的方法。

线程等待:Object 类中的wait() throws InterruptedException 方法,导致当前的线程等待,直到其
他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法
线程唤醒:Object 类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的。
Object类中的notifyAll()方法,唤醒在此对象监视器上等待的所有线程。
wait()、notify()、notifyAll()这三个方法只能在被同步化(synchronized)的方法或代码块中调用。
程序例子:
生产者:
synchronized (resource) {
//1.循环检查资源是否已满
while(resource.isFull()){ //当资源满了
System.out.print(this.getName()+"说:"); //等价于Thread.currentThread.getName()
System.out.println(resource.getName()+"已经生产满了,停止生产,等待你来消费");
resource.wait(); //实际是让生产者等待,也就是Producer的对象等待
/**
* 不能用sleep---因为它会一直等待;
* 不能用join,因为它需要另一个线程,而这里没有另一个线程
* 用wait因为,直到其他线程调用此对象的 notify() 方法或 notifyAll()就会被唤醒
*/
}
//2.接消费者发来的通知
resource.setNum(resource.getNum() + 1);
System.out.println(this.getName()+"正在生产第"+resource.getNum()+"个"+resource.getName());
//发出通知,让所有消费者可以来消费
resource.notifyAll(); //让所有的等待resource.wait()的线程唤醒该线程
}
消费者:
synchronized (resource) {
//1.循环检查资源是否为空
while(resource.isEmpty()){ //如果资源空了,就要停止消费,通知生产者生产
System.out.print(this.getName()+"说:"); //当前线程
System.out.println(resource.getName()+"已经消费完了,停止消费,通知生产者生产");
resource.wait();
}
//2.进行消费,接生产者发来的通知
resource.setNum(resource.getNum()-1);
System.out.println(this.getName()+"正在消费,"+resource.getName()+"还剩"+resource.getNum()+"个");
//发出通知,让左所有生产者中的随机一个可以生产(后面的生产者就会去抢,没抢到就继续等)
resource.notifyAll();
}