- 什么是线程?
- Java中的线程类Thread
- 线程安全问题
- 常用的方法
- 线程的生命周期
什么是线程?
- 在说线程之前,必须要说到进程,进程就是具有一定独立功能的程序,是操作系统进行资源分配和调度的一个独立单位;而线程是进程的一个实体,是 cpu 调度和分派的基本单位,是比进程更小的可以独立运行的基本单位;
- 我们的计算机的CPU一般都是多线程的,比如四核八线程,表示CPU在同一时间最多可以并行8个线程;我们在开发应用程序的时候使用多线程编程,也能更大程度发掘CPU性能,提高用户体验;
- 当然也不能一味追求性能滥用多线程,毕竟,线程间安全是一个棘手的问题,而且我们一个程序产生了太多线程去抢占CPU资源,对其他程序貌似不是特别友好。
Java中的线程类Thread
线程类的构造方法
//分配一个新的线程对象
public Thread()
//分配一个指定名字的新的线程对象
public Thread(String name)
//分配一个带有指定目标新的线程对象
public Thread(Runnable target)
//分配一个带有指定目标新的线程对象并指定名字
public Thread(Runnable target,String name)
线程类的常用API
//获取当前线程名称
String getName()
//通知JVM开启线程开始执行run方法
void start()
//线程任务入口,比如主线程是main
void run()
//使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),如果有锁也不释放锁
static void sleep(long millis)
//返回对当前正在执行的线程对象的引用
static Thread currentThread()
-
每一个执行线程都有一片自己所属的栈内存空间,进行方法的压栈和弹栈,在Java中创建线程的方式有多种:
①实现Runnable接口的方式创建新线程任务,当然这与匿名内部类的方式创建新线程任务类似;
②通过继承 Thread 类的方式;
③通过实现 Callable 接口(Java 5 之后)。
-
由于Java是单继承模式,只能继承一个父类,因此继承 Thread 类创建新线程任务的方式局限性就比较大,因此使用实现Runnable接口的方式是比较推荐的,使用这种方法还降低了线程任务对象和线程对象的耦合性;
使用实现Runnable接口的方式来创建一个线程任务:
public class MyRunImpl implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "........" + i);
}
}
}
在main主线程中运行线程任务:
public static void main(String[] args) {
MyRunImpl task = new MyRunImpl();
MyRunImpl task1 = new MyRunImpl();
//Thread类的对象调用start方法,开启线程
Thread t = new Thread(task,"线程1");
//可以同时给线程一个名字
Thread t1 = new Thread(task1,"线程2");
//直接调用run()不会开启新线程,只有调用start()才会开启新线程
t.start();
t1.start();
}
执行输出结果:
线程1........0
线程2........0
线程1........1
线程2........1
线程1........2
线程2........2
线程1........3
线程2........3
......
线程安全问题
-
使用了多线程其实就避免不了线程安全问题,就像有一块蛋糕,一个人吃没什么问题,很多人一起吃就会产生很多问题,一起吃还是一个个吃?你吃奶油还是吃面包部分?一共5个人怎么切才能均等分?
-
Java代码中解决多线程安全问题有几种方式,同步代码块,同步方法,以及Lock接口的方式。
同步代码块
- 同步代码块的声明就是在代码块的前面加上synchronized;
synchronized (<锁对象>) {
//可能会产生线程安全问题的代码
}
- 同步代码块中的锁对象可以是任意的对象;但多个线程时,要使用同一个锁对象才能够保证线程安全。
同步方法
<修饰符> synchronized <返回值类型> <方法名称>(<参数列表...>){
//可能会产生线程安全问题的代码
}
- 对于非静态方法,同步锁就是this(对象本身);
- 对于静态方法,我们使用当前方法所在类的字节码文件对象为同步锁。
Lock接口
- 接口在java.util.concurrent.locks.Lock下,需要在使用时手动获取锁和释放锁;
//获取锁对象
abstract void lock()
//释放锁对象
abstract void unlock()
- 其实无论使用上述哪种方式,都使用到了一个“锁”,而解决线程问题就离不开这个锁;可以理解为,在线程执行到可能出线程安全问题的代码的时候,一个线程只有拿到了这个锁,才能被允许去执行,不然我就只能等着别的线程执行完了还给我这个锁;自己拿到锁去执行完代码后也要交出这个锁;因此这就是为什么要保证在这些方式中锁对象必须唯一;要是有两把锁的话,那就没有意义了;
常用的方法
wait()与sleep()
- wait()是object的方法,调用此方法则放弃锁并进入等待执行状态(我歇会儿,你们先执行着);而sleep()是线程类的方法,掉用后会暂停此线程的执行,但是不释放锁(我不执行,别人也别想执行);
- sleep()在代码的任何地方都可以使用,而wait()只能在同步方法或者同步代码块中使用;
- sleep()在执行的时候会给一定的时间,时间到了就自动醒来继续执行,而wait()必须需要别人执行notify()方法来唤醒,而唤醒的时候并不给你锁,还得等待获取到锁才能真正执行。
- 在使用锁机制的时候逻辑要非常清晰明白,不然很容易导致两个线程都在等待对方执行完释放锁而处于无限等待状态,也就是死锁(锁死了);
yield()
- yield()称为线程让步方法,调用后会暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
join()
- 在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态;即我执行到一半想让你帮忙,让你先执行,然后等你帮忙完了,我再执行,但是因为丢掉了锁,因此只能等待锁到后才能真正执行起来。
线程的生命周期
- 新创建:当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
- 可运行(就绪):当调用线程对象的start()方法,线程即进入可运行(就绪)状态。处于可运行(就绪)状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
- 锁阻塞:处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到可运行(就绪)状态,才 有机会再次被CPU调用以进入到运行状态;
- 无限等待:也属于阻塞范畴**(等待阻塞)**;调用wait()方法后如果没有被唤醒,将一直处于无限等待的阻塞状态;
- 计时等待:调用sleep(时间)或者wait(时间)的时候进入到计时等待,这个等待是可以看到头的,知道什么时候等待结束;当然wait()等待时间未到也可以被其他程序唤醒;
- 终止:程序运行结束。
为什么在这里没有运行状态呢,因为实际的运行是操作系统和CPU调度决定的,JVM只需要告诉操作系统,这个进程已经准备就绪了,可以被调度执行了就可以;
注意:本文归作者所有,未经作者允许,不得转载