多线程、同步锁、定时器

本文详细介绍了Java中的进程、线程概念,包括多线程的两种主要实现方式(继承Thread和实现Runnable接口)、Callable接口的应用、线程池、线程通信机制(wait/notify)、线程同步与synchronized和Lock的区别,以及线程的生命周期和常见问题如死锁。
摘要由CSDN通过智能技术生成

多线程

进程与线程的区别

进程

一个应用程序(1个进程是一个软件),是系统进行资源分配和调用的独立单位。

线程

一个进程中的执行场景/执行单元,是进程中的单个顺序控制流,是一条执行路径。

一个进程可以包括多个线程。

多线程的实现方式

1.继承Thread类

1.创建自定义类 继承 Thread
2.重写run方法,并再run方法中实现线程中的执行逻辑
3.创建自定义类的对象 再执行其start方法 
    
    
public class CreateThread extends Thread {
    // run方法中编写 多线程需要执行的代码
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("i:" + i);
        }
    }

    public static void main(String[] args) {
        // 1.创建一个线程
        CreateThread createThread = new CreateThread();
        // 2.开始执行线程 注意 开启线程不是调用run方法,而是start方法
        createThread.start();
    }
}

2.实现Runnable接口

1.创建自定义类 实现其接口
2.重写run方法
3.创建 Thread对象,并将自定义类的对象传入 Thread构造方法中 
4.通过Thread对象调用start方法
    
public class CreateRunnable implements Runnable {

    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("i:" + i);
        }
    }

    public static void main(String[] args) {
        // 1.创建一个线程
        CreateRunnable createThread = new CreateRunnable();
        // 2.开始执行线程 注意 开启线程不是调用run方法,而是start方法
        Thread thread = new Thread(createThread);
        thread.start();
    }
}

Thread与Runnable比较:第一种通过继承Thread重写run方法实现的,受Java单继承的限制,无法继承其他类,而第二种方法是实现Runnable接口,可以继承其他类,多个线程共享同一个变量时更加方便。

3.实现Callable接口

1.创建自定义类实现其接口
2.重写其call方法 注意:对于call方法具有返回值 其返回值类型可以再Callable接口上给定其泛型
3.构建线程池对象,使用submit对当前自定义类对象进行提交执行
4.通过shutdown对当前线程进行关闭操作

public class MyCallable implements Callable<Integer> {
@Override
public Integer call() {
    System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
    return 1;

4.匿名内部类

1.new Thread(){重写其run方法} 获取到对象并对其进行start
    2.new Thread (new Runable 匿名内部类对象传入)之后进行start启动
    
public class CreateRunnable {

    public static void main(String[] args) {
        //创建多线程创建开始
        Thread thread = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("i:" + i);
                }
            }
        });
        thread.start();
    }
}

Runnable接口与Callable接口的联系和区别:

联系:

  • 都是接口
  • 都可以编写多线程程序
  • 都采用Thread.start()启动线程

区别:Runnable 接口 run 方法无返回值;Callable 接口 call 方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息
注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。

Thread类的构造方法

public Thread(ThreadGroup group, Runnable target, String name)

ThreadGroup可以指定当前线程属于哪个线程组,对于线程组的使用,创建ThreadGroup()对象,括号内传入线程组的名称,在创建线程时传入该名称即可把线程归属到该名称的线程组内。

Runnable可以传入一个实现了Runnable接口的对象,为这个对象创建线程

String name可以设置当前线程的名称

多线程常用方法

  1. 获取线程的名称

    //这段代码被某个线程执行,返回的就是对应线程的名称
    Thread.currentThread().getName()
    
    对象名.getName()
    例如有一个Student对象,为这个Student对象创建线程,在创建线程时可以传入线程名
        
    Student student = new Student();
    Thread thread = new Thread(student,"1");
    thread.getName()//可以获取到当前线程的线程名
    
  2. 设置线程名

1.new Thread(Runnable接口实现对象,"名称")
2. 创建一个继承了Thread类的对象,通过其构造方法 进行传递,在构造方法中使用 super(线程名变量)
public Student(String name) {
        super(name);
}
3.使用Thread对象.setName添加
  1. 启动线程
3.start 用于启动一个线程,执行run方法中的逻辑
 可以使用Thread对象名.start()的方式调用
  1. 线程睡眠阻塞
.sleep  给定一个倒计时时间阻塞当前线程
因为sleep是静态方法,故可以使用Thread类名.方法名的形式调用
  1. join
5.join 当给定的线程使用join之后,该线程会一直占用当前CPU的执行权,直到其线程死亡,释放CPU的执行权。给定一个时间参数,可以限制当前线程的执行时间,后续和其他线程竞争执行权。
  1. yield
6.yield 线程礼让,让同级的或优先级较高的线程先运行,非常短的时间当程序执行到该方法时,会将当前线程的CPU执行权进行释放,再进行下一轮的CPU执行权的竞争
  1. stop
7.stop 当被调用时,当前线程执行结束,并不影响其他线程执行
  1. interrupt
8.interrupt 对于该方法执行后,不会像stop方法立即终止当前线程,而是会等待一小段时间后,再去停止线程
9.setDaemon 对于守护线程,也称为后台线程,当该线程执行完成后,整个应用程序退出,若其他线程未执行完成,抛出一个异常
  1. 设置线程的优先级
thread.getpriority():获取线程的优先级  默认5
thread.setPriority():设置线程的优先级,值越大优先级越高

设置线程的优先级后只能保证谁优先执行并不能保证谁先执行完成。

wait与sleep区别

  1. sleep()方法,是属于Thread类中的。
  2. wait()方法,则是属于Object类中的。
  3. sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是没有释放锁
  4. wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

多个线程之间的问题

  1. 对多个线程进行操作时,由于多个线程互相抢占cpu资源,可能导致某个线程在执行过程中被其他线程打断。

  2. 当多个线程操做同一个变量时,由于多个线程之间存在抢占,可能导致一个线程的一次执行还没结束时,被另一个线程抢占,可能会导致对这个变量的操做结果出现错误。

死锁

死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程),简单来说就是多个线程之间互相竞争cpu资源,各自持有对方的资源,互相等待。

线程锁

syncronized锁

syncronized:同步锁,可以修饰代码块,也可以成员修饰方法,静态方法。对于syncronized修饰run方法后,只有一个线程在运行。

  1. 修饰成员方法:成员方法 对当前对象进行加锁操作,优点是可以直接对当前对象进行加锁,缺点是要求当前对象只能有一个 比较清晰简介。
  2. 修饰静态方法:对当前方法的类进行加锁,类加锁后其类产生的对象也会被加锁 ,静态方法的类进行加锁,其对象可以有很多个 。
  3. 修饰同步代码块:可以指定具体的对象进行加锁操作,可以指定任意一个对象进行加锁,需要对对象进行传递,并且对象只能有一个 。

同步代码块:

synchronized(){}

	()中可以传入一个对象,对对象进行加锁操做,被加		锁的对象要有唯一性,

	{}写入代码块

方式1:()使用this传入自身类的对象

方式2:在本类创建Object对象,在()传入一个Object对象

方式3:创建一个Object对象的变量,把变量写入构造函数

Lock锁

Lock锁是java.util.concurrent.locks中的一个接口。Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。

使用lock和Unlock方法 获取 Lock类的子类之后,对对象调用lock方法,当前lock和unlock之间的程序会被加锁 。

使用方法:创建new ReentrantLock()对象,用对象名.lock()或对象名.unlock()的方式加锁,或解锁。

private Lock lock = new ReentrantLock();
    
    //使用完毕释放后其他线程才能获取锁
    public void lockTest(Thread thread) {
        lock.lock();//获取锁
        try {

        } catch (Exception e) {

        }finally {
            lock.unlock(); //释放锁
        }
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockTest {
    private Lock lock = new ReentrantLock();

    //使用完毕释放后其他线程才能获取锁
    public void lockTest(Thread thread) {
        lock.lock();//获取锁
        try {
            System.out.println("线程" + thread.getName() + "获取锁");
        } catch (Exception e) {
            System.out.println("线程" + thread.getName() + "发生了异常");
        } finally {
            System.out.println("线程" + thread.getName() + "执行完毕释放锁");
            lock.unlock(); //释放锁
        }
    }

    public static void main(String[] args) {
        LockTest lockTest = new LockTest();
        Thread thread1 = new Thread(new Runnable() {

            public void run() {
                lockTest.lockTest(Thread.currentThread());
            }
        }, "线程一");

        Thread thread2 = new Thread(new Runnable() {

            public void run() {
                lockTest.lockTest(Thread.currentThread());
            }
        }, "线程二");
        // 启动2个线程
        thread2.start();
        thread1.start();
    }
}

synchronized与Lock的区别

1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;

  1. synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  2. synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  3. 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,自动放弃的方法tryLock(),具有更完善的错误恢复机制。
  4. Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

线程通信

什么是多线程之间的通讯

在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步。通信是指线程之间以如何来交换信息。一般线程之间的通信机制有两种:共享内存和消息传递。

wait、notify和方法
  1. 因为涉及到对象锁,他们必须都放在synchronized中来使用. wait、notify一定要在synchronized里面进行使用。
  2. wait() :使当前线程处于等待状态,执行到该代码时当前线程处于等待状态,需要其他线程来唤醒当前线程的执行。
  3. notify() :唤醒因锁池中的线程,使之运行,nofity()可以唤醒一个同步锁锁住的同一个对象或同一个类。
//输入类
class InputThread extends Thread {
    private User user;

    public InputThread(User user) {
        this.user = user;
    }

    public void run() {
        int count = 0;
        while (true) {
            synchronized (user) {
                if (user.sign == true) {
                    try {
                        // 当前线程变为等待,但是可以释放锁
                        user.wait();
                    } catch (Exception e) {

                    }
                }
                if (count % 2 == 0) {
                    user.name = "张三";
                    user.sex = "男";
                } else {
                    user.name = "小紅";
                    user.sex = "女";
                }
                count += 1;
                user.sign = true;
                // 唤醒当前线程
                user.notify();
            }
        }
    }
}
//输出类
class OutThread extends Thread {
    private User user;

    public OutThread(User user) {
        this.user = user;
    }

    public void run() {
        while (true) {
            synchronized (user) {
                if (user.sign==false) {
                    try {
                        // 当前线程变为等待,但是可以释放锁
                        user.wait();
                    } catch (Exception e) {

                    }
                }
                System.out.println(user.name + "====" + user.sex);
                user.sign = false;
                // 唤醒当前线程
                user.notify();
            }
        }
    }
}	

多线程的生命周期

  1. 创建对象时,该对象处于新建状态
  2. 当使用start方法启动一个线程时,那么该线程处于就绪状态,可以竞争获取CPU的执行时间片段
  3. 当获取到执行权限时,就处于运行状态,如果run方法执行结束,之后线程处于死亡状态
  4. 当获取到执行权限时,就处于运行状态,运行过程中如果遇到sleep那么就处于睡眠阻塞状态,睡眠状态结束后,再进入就绪状态
  5. 当获取到执行权限时,就处于运行状态,运行过程中如果遇到wait那么就处于等待阻塞,那么后续需要相同锁的其他线程对其进行唤醒操作 唤醒操作需要使用 notify函数,再进入就绪状态
  6. 当获取到执行权限时,就处于运行状态,运行过程中如果遇到synchronized关键字,并且加锁的内容没有释放锁,那么当前处于同步阻塞状态等待其他线程对当前锁进行释放,之后再处于就绪状态

线程池

ExecutorService.newFixedThreadPool( )传入int类型参数表示开启几个空线程。

线程池对象名.submit(),传入要开启线程的对象,创建指定对象的多个线程,可以执行线程池类对象的run()方法。

线程池名.shutdown():所有的线程执行结束后,对线程进行关闭。

定时器

指定之间执行某个应用程序

Timer: 创建对象后可以通过schedule方法设置一个定时器

.schedule()方法可以传入三个参数,

TimerTask对象

delay:第一次延迟执行的时间

以及period周期:传入参数表示多久运行一次。

TimerTask:创建一个定时器的执行任务,需要重写器run方法,在定时器触发时,会执行run方法中的逻辑。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值