基本概念
- 程序(program) : 是为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态 的代码,静态对象
- 进程(process) : 程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,有它自身的产生、存在、消亡的过程 也就是生命周期。进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
- 线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径,若一个进程同一时间并行执行多个线程,就是支持多线程,线程作为调度和执行的单位,每个线程用于独立的运行栈和程序计数器,线程切换的开销小,这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能会带来安全的隐患。
- 单核CPU:是一种假的多线程,在同一单元时间内,只能执行一个线程的任务,但CPU时间单元特别短,因此感觉不出来。
- 多核CPU:多核CPU才能更好的发挥多线程的效率
- 并行:多个CPU同时执行多个任务
- 并发:一个CPU(采用时间片)同时执行多个任务,比如购物的秒杀活动,抢票等。
使用多线程的好处
1.提高应用程序的响应,对图形化界面更有意义,可增强用户体验。
2.提高CPU的利用率
3.改善程序结构,将长又复杂的进程分为多个线程,独立运行,利于理解和修改
创建线程
- 方式1:继承Thread类
步骤
1.创建一个继承Thread类的子类
2.重写Thread类中的run方法,将线程执行的操作写在该方法内
3.创建Thread类的子类的对象
4.通过此对象调用start()方法:启动当前线程(会自动调用run())
写法:
public class classByThread extends Thread(){
@Override
public void run(){
//代码
}
public static mian(String[] args){
classByThread c = new classByThread();
//启动线程
c.start();
}
}
注意:启动线程必须调用start() ,不能用调用run方法的方式启动线程
如果再启动一个线程,必须重新创建一个Thread类的子类,调用此对象的start()
- 方式2:实现Runnable接口
步骤
1.创建一个实现Runnable接口的类
2.实现类去实现Runnable中的抽象方法:run()
3.创建实现类的对象
4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5.通过Thread类的对象调用start(),开启线程
写法:
public class ClassByIm implements Runnable{
@Override
public void run(){
//代码
}
public static void main(String args){
ClassByIm c = new ClassByIm();
//创建Thread类对象
Thread th = new Thread(c);//把实现类对象传进去
th.start();//通过Thread对象调用start方法
}
}
方式1 VS 方式2
优先选择方式2
原因: 1.实现的方式没有类的单继承性的局限性
2.实现的方式更适合来处理多个线程共享数据的情况
相同点:两种方式都需要重写run方法,将线程要执行的逻辑声明在run方法内,都是调用start方法
Thread类中常用的方法
- start():启动当前线程、调用当前线程的run方法,注意:一个对象只能启动一次线程,否则抛异常
- run():通常需要重写Thread类中的此方法,将创建线程要执行的操作都声明在此方法内
- currentThread():静态方法,返回当前执行代码的线程
- getName():获取当前线程的名字
- setName():设置当前线程的名字
- yield():释放当前CPU的执行权,线程让步
- join():阻塞,线程a中调用线程b的join() 此时线程a进入阻塞状态,直到线程b完全执行完以后,线程a结束阻塞状态
- sleep(long time) : 让该线程,暂停指定毫秒时间后再执行,抛出InterrupdatedException异常
- stop():强制线程结束
- isAlive():返回boolean,判断线程是否活着。
- getPriority():返回线程的优先级
- setPriority(int a):设置线程的优先级,最小1,最大10,默认是5
- 最小:MIN_PRIORITY
- 最大:MAX_PRIORITY
- 默认:NORM_PRIORITY
- 注意:线程创建时继承父线程的优先级,低优先级只是获取调度的概率低,不一定是在高优先级线程之后才被调用。
stop() 和suspend()
stop()和suspend()方法已经停止使用了
因为:stop()方法不安全,stop方法强制中止线程,将会释放该线程对象已经锁定的所有监视器,如果这些监视器保护的任何对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们,结果很难查出问题所在
suspend()具有固定的死锁倾向,调用suspend方法,目标线程就会停下来,但仍然持有在这之前获得的锁定,此时,其他任何线程都不能访问锁定的资源,除非被挂起的线程恢复运行,对任何线程来说,如果想恢复目标现成,同时又试图使用任何一个锁定的资源,就会造成死锁,所以不应该使用suspend方法,而应该在自己的Thread类中置入一个标志,指出线程应该运行还是挂起,如果标志指出线程应该挂起,就用wait方法让线程进入等待状态,如果标志指出线程应该运行,则用notify方法重新启动线程
线程的分类
线程分为守护线程和用户线程
两种方式唯一的区别是判断JVM什么时候离开
守护线程是用来服务用户线程的,通过start()方法调用
Thread对象.setDaemon(true),可以把用户线程变成一个守护线程
守护线程守护的是所有非守护线程
例子:java垃圾回收就是一个典型的守护线程,若JVM中都是守护线程,当前JVM退出
线程的生命周期
JDK中用Thread.State类定义了线程的几种状态
要想实现多线程,必须在主线程中创建新的线程对象。java语言使用Thread类及其子类的对象表示线程,在它的一个完整的生命周期中通常要经历五种状态
新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被认为挂起或执行输入输出操作时,让出CPU并临时终止自己的执行,进入阻塞状态(sleep,join,wait)
死亡:线程完成了全部工作或线程被提前强制性地中止或出现异常导致结束(stop、报错或异常、run方法完成),死亡是最终状态,阻塞不是
线程的同步机制
线程不同步就会导致线程安全问题
举例: 假设有五个窗口,共同卖一百张电影票,如果这几个窗口不同步,则,每个窗口各卖各的,会导致,重票的问题
解决: 可以把100张票,放在一个位置,那个窗口有人买票,售票员就去那拿,这样,其他的售票员只能,等他完成后,再开始拿
实现同步机制的方式
- 方式1:同步代码块
写法:
synchronized(同步监视器){
//需要被同步的代码
}
注意:1.操作共享数据的代码。即为需要被同步的代码-->不能包含代码多了,也不能少了
2.共享数据: 多个线程共同操作的变量
3.同步监视器:也叫锁,任何一个类的对象,都能充当锁,要求多个线程必须要共用同一把锁
4.在实现runnable接口创建多线程时,可以考虑使用this当锁
5.在继承Thread类创建多线程的方式中,慎用this当锁,可以考虑使用当前类(反射)当锁 类名.class的方式当锁
- 方式2:同步方法
如果操作共享数据的代码完整的声明在一个方法中,可以将此方法声明同步的
注意:
1.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明
2.非静态的同步方法,同步监视器:this
静态的同步方法,同步监视器:当前类本身
写法
public (static) synchronized 类型 方法名(){
//同步代码
}
- 方式3:Lock锁
JDK5.0新增Lock接口,用于解决线程的安全问题
使用Lock接口的实现类ReentrantLock
1.创建RenntrantLock对象
2.在try内,通过lock对象调用lock()方法
3.在finally调用解锁方法 lock对象.unlock()
代码
class TestLock implements Runnable{
//1.创建Lock子类对象
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
//2.调用lock()方法
try {
lock.lock();
//要同步的代码
} finally {
//3.调用解锁方法
lock.unlock();
}
}
}
使用三种方式解决卖票问题
方式1:使用同步代码块
class Windo implements Runnable{
private int ticket = 100;
private Object obj = new Object();//实现的方式不需要用static,继承Thread的方式需要使用static关键字,保持唯一
@Override
public void run() {
while(true){
synchronized(obj){//实现类的方式可以用this当锁,但继承Thread的方式不能,可以用反射当锁 类名.class
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":买票,票号为:"+ticket);
ticket--;
}else{
break;
}
}
}
}
}
public class ThreadTest3 {
public static void main(String[] args) {
//3.创建实现类的对象
Windo mt = new Windo();
//4.再创建Thread类对象,将实现类的对象传进构造器
Thread th = new Thread(mt);
Thread th1 = new Thread(mt);
//5.通过Thread类的对象,调用start()方法
th.start();
th1.start();
}
}
方式2:使用同步方法
class Wind implements Runnable{
private int ticket = 100;
Object obj = new Object();
@Override
public void run() {
while(true){
show();
}
}
public synchronized void show(){
if(ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":买票,票号为:"+ticket);
ticket--;
}else{
return;
}
}
}
public class ThreadTest4 {
public static void main(String[] args) {
//3.创建实现类的对象
Wind mt = new Wind();
//4.再创建Thread类对象,将实现类的对象传进构造器
Thread th = new Thread(mt);
Thread th1 = new Thread(mt);
//5.通过Thread类的对象,调用start()方法
th.start();
th1.start();
}
}
方式3:使用Lock锁
public class ThreadTest6 {
public static void main(String[] args) {
w w1= new w();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.start();
t2.start();
t3.start();
}
}
class w implements Runnable{
private int ticket = 100;
//1.创建Lock子类对象
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
//2.调用lock()方法
try {
lock.lock();
if(ticket>0){
System.out.println(Thread.currentThread().getName()+":买票,票号为:"+ticket);
ticket--;
}else{
break;
}
} finally {
//3.调用解锁方法
lock.unlock();
}
}
}
}
三种方式的区别
Lock是显示锁(手动开启和关闭),synchronized是隐式锁,出了作用域自动失效
Lock只有代码块锁,synchronized有代码块锁和方法锁
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)
优先使用Lock其次同步代码块最后同步方法
死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,这样就形成了线程的死锁
注意:
1.出现死锁后,不报错,不出异常,不会出现提示,只是线程都处于阻塞状态,无法继续运行
2.使用同步时,要避免出现死锁
解决办法
减少对同步锁的定义
使用算法
线程通信
- wait():一旦执行此方法,当前线程就进入阻塞状态
- notify():唤醒被wait的一个线程,若多个线程被wait() 则按照优先级唤醒
- notifyAll() : 唤醒所有被wait()的线程
注意
1.wait()、notify()、notifyAll() 三个方法必须使用在同步代码块或同步方法中
2.wait()、notify()、notifyAll() 三个方法的调用者必须是同步代码块或同步方法的同步监视器,否则会出异常
3.wait()、notify()、notifyAll() 三个方法是定义在java.lang.Object类中的
sleep() 和wait的区别
相同: 一旦执行方法,就让当前线程进入阻塞状态
不同:
1.两个方法声明的位置不同,Thread类中声明sleep,Object类中声明wait
2.调用的要求不同,sleep可以在任何需要的场景下调用,而wait只能使用在同步代码块或同步方法中
3.如果两个方法都被使用在同步代码块或同步方法中,sleep不会释放锁,而wait会释放锁
JDK5.0新增线程创建的方式
1.实现Callable接口
步骤
1.创建Callable接口的实现类
2.重写Callable接口的call方法
3.创建Callable实现类的对象
4.创建FutureTask 对象,并将实现类的对象传给它
5.创建Thread对象,并将futureTask的对象传过去,调用start()方法
6.通过FutureTask对象.get()接收Callable的返回值
写法
class num1 implements Callable{
public Object call() throws Exception{
代码
}
}
......mian(){
num1 n = new num1();
FutureTask fk = new FutureTask(n);
new Thread(fk).start();
try{
Object obj = fk.get();
}cathc(){
}
}
Callable的特点
1.call方法有返回值
2.call方法可以抛出异常,被外面的操作捕获,获取异常信息
3.Callable支持泛型
2.使用线程池
步骤
1.创建Runnable或Callable的实现类
2.重写run或call方法
3.提供指定数量的线程池
3.1设置线程池的属性
4.执行指定的线程
5.关闭线程池
写法
class num1 implements Runnable{
public void run(){
//代码
}
}
main(){
//提供指定的线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池的属性
service1.setCorePoolSize(15);
//service1.setKeepAliveTime();
.....
service1.execute(new num1()) //该方法适用于Runnable
service1.submit(Callable) // 该方法适用于Callable
//关闭线程池
service1.shutdown();
}
好处
1.提高了响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程不需要每次都创建)
3.便于线程管理
corePoolSize: 核心池的大小
maximumPoolSize:最大线程数
keepAliveTime: 线程没任务时最多保持多长时间后会中止
ExecutorService
ExecutorService 线程池接口,常见子类ThreadPoolExecutor
* void execute(Runnable) 执行任务/命令
* <T>Future<T> submit(Callable) 执行任务,有返回值
* void shutdown():关闭线程池
Executors 工具类,线程池的工厂类,用于创建并返回不同类型的线程池
* Executors.newCachedThreadPool() :创建一个可根据需要创建新线程的线程池
* Executors.newFixedThreadPool():创建一个可重用固定线程数的线程池
* Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
* Executors.newScheduledThreadPool(n):创建一个线程池,可以给定延迟后运行命令或定期地执行
线程安全的懒汉式
class Bank{
private Bank(){}
private static Bank instance = null;//public static synchronized Bank getInstance() {
public static synchronized Bank getInstance() {
//方式1:效率稍差
// synchronized (Bank.class) {
// if(instance ==null){
// instance = new Bank();
// }
// return instance;
// }
//方式2:效率高
if (instance == null) {
synchronized (Bank.class) {
if(instance ==null){
instance = new Bank();
}}
}
return instance;
}
}
总结
线程的创建方式有四种:一类一池俩口(Thread类、线程池、Runnable接口、Callable接口)
本人是Java初学者,水平有限,本文章中如果有不对的地方,麻烦您能指出来。向您表示感谢。