目录
前言:本人为CS大三学生,目前属于学习阶段(小白),有问题请大佬们指正。
一、基本概念
在学习多线程前,我们首先要知道这样几个概念。
1、程序:为完成特定任务,用某种语言编写的一组指令集合。即为一段静态(没有加载到内存空间,CPU没有参与运算)的代码。
2、进程:见文知意,即为正在运行的程序。在计算机操作系统中,进程是一个可拥有资源的基本单位。
3、线程:一个程序内部的一条执行路径。作为调度和分派的基本单位。
对以上举个概念栗子:
当你下载了某个杀毒软件装在电脑上还未运行时,杀毒软件即为一个程序,点击运行后系统为其分配运行需要的资源,此时即为进程。你可以用它同时进行杀毒、清理垃圾...,此时杀毒占用一条线程,清理垃圾占用一条线程。
二、线程的创建和使用
方式一 继承Thread类
步骤:
1、继承Thread类
2、重写run()方法,run()方法中写多线程的代码
3、创建继承Thread类的子类对象
4、通过此对象调用start()方法
说明 :start()方法的作用,启动当前线程,调用当前线程的run();
例:主线程打印0-100的奇数,副线程打印0-100的偶数
//继承Thread类
class MyThread extends Thread{
//重写run()
@Override
public void run(){
for (int i = 0; i < 100; i++) {
if(i % 2 == 0) {
System.out.println(i);
}
}
}
}
public class ThreadTest{
public static void main(String[] args) {
//创建对象
MyThread t1 = new MyThread();
//调用start()
t1.start();
for (int i = 0; i < 100; i++) {
if(i % 2 != 0) {
System.out.println(i);
}
}
}
}
此方式创建多线程需要注意:对于多个线程操作的数据要用static关键字修饰,使其只有一份。
方式二 实现Runnable接口
步骤:
1、创建实现Runnable接口的类
2、实现Runnable接口的类实现run()方法
3、创建实现Runnable接口类的对象
4、将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5、通过Thread类的对象调用start()
例:
//1 实现Runnable接口
class MyThread2 implements Runnable{
//2 重写run()方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0){
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
}
}
public class MyThread2Test {
public static void main(String[] args) {
//3 创建 实现Runnable接口的类 的对象
MyThread2 myThread1 = new MyThread2();
//4 将此对象作为参数传递到Thread类的构造器中,船舰Thread类的对象
Thread t1 = new Thread(myThread1);
//5 通过Thread类的对象调用start()
t1.start();
for (int i = 0; i < 100; i++) {
if(i % 2 != 0) {
System.out.println(i);
}
}
}
}
上面我们说到 start()方法的作用是启动当前线程,调用当前线程的run();
此时当前Thread的run()并没有实现,是怎样调用的呢?
我们可以看到JDK中Thread的一个构造方法,将Runnable接口对象传入。
Thread类中的run()方法判断(Runnable) target是否为空,不为空则调用Runnable的run()
方式三 实现Callable接口
官方文档中Callable接口实现多线程比Runnable更为强大。
原因
· call()可以有返回值 · call()可以抛出异常 · Callable支持泛型
Callable实现多线程
步骤
1、实现Callable接口
2、重写call方法
3、创建Callable接口实现类的对象
4、将Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
5、将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象
6、调用start()
[ 可选 7、用get()方法获取call()方法的返回值 ]
//1实现Callable接口
class NumThread implements Callable{
@Override
//重写call()方法
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
if (i % 2 ==0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class CallableTest {
public static void main(String[] args) {
//3 创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
//说明 FutureTask 也实现了 Runnable接口
FutureTask futureTask = new FutureTask(numThread);
//5 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
try {
//6 get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
Object sum = futureTask.get();
System.out.println("0-100偶数的总和"+sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
ps:我并不能理解Callable接口的方式。仅仅停留于知道它启动多线程的步骤。
方式四 线程池
先说一下线程池的优势
· 降低资源消耗。重复利用已创建的线程降低创建线程所带来的消耗
· 提高响应速度。不需要等待线程创建即拿即用。
· 提高线程的可管理性。线程资源不进行管理的情况下,若无限制创建会加大系统资源的消耗,且降低系统的稳定性,线程池可以对线程进行统一的管理,分配和调优。
步骤
1、创建线程池
2、创建实现Runnable接口或Callable接口的类并从写它们的抽象方法
3、用线程池对象调用execute(Runnable runnable )或submit(Callable callable)
4、不用线程池后,关闭线程池。
public class ThreadPool {
public static void main(String[] args) {
//1、提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
// 用于管理线程池
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
// service1.setCorePoolSize(15)
//2、执行只从的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
//适合适用于Runnable
service.execute(new NumberThread());
//适合适用于Callable
service.submit(new NumberThread2());
//3、关闭线程池
service.shutdown();
}
}
class NumberThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0){
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
}
}
class NumberThread2 implements Callable {
@Override
public Object call() throws Exception {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0){
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
return null;
}
}
Executors中提供创建线程池的方式
newFixedThreadPool
创建一个线程池,该线程池重用固定数量的从共享无界队列中运行的线程。
newCachedThreadPool
创建一个根据需要创建新线程的线程池,但在可用时将重新使用以前构造的线程。
...
三、线程同步问题
什么是线程安全问题?
多个线程同时操作共享数据(临界资源)时引起的混乱结果;
ps:测试时我们可以用sleep()方法模拟网络延迟,放大问题的发生性;
synchronized解决线程安全问题
1、同步代码块
synchronized(同步监视器){
}
说明
>操作共享数据的代码,即为需要被同步的代码
>共享数据:多个线程共同操作的变量。
>同步监视器,俗称:锁。任何一个类的对象都可以充当。
同步监视器ps:{多个线程必须共用一把锁,即共同的是同一个同步监视器。否则无效}
例:三个线程买票
实现Runnable的方式
public class Window1 implements Runnable {
private int ticket = 100;
//Object obj = new Object(); 可以使用obj充当同步监视器
@Override
public void run() {
while (true) {
synchronized (this) {
if (ticket > 0) {
try {
Thread.sleep(60);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "您拿到了第" + ticket + "张票");
ticket--;
} else {
break;
}
}
}
}
}
class WindowTest1 {
public static void main(String[] args) {
Window1 window1 = new Window1();
Thread thread1 = new Thread(window1,"甲");
Thread thread2 = new Thread(window1,"乙");
Thread thread3 = new Thread(window1,"丙");
thread1.start();
thread2.start();
thread3.start();
System.out.println("--------------");
}
}
这里的同步监视器用this,此时的this即为window1,此时thread1、thread2、thread3三个线程公用的都是同一个同步监视器,可以达到同步效果。
注释部分用Object的对象充当同步监视器也可以达到通用的效果。通常情况,在实现Runnable接口的方法中直接使用this充当同步监视器,不需要新建对象浪费资源。
继承Thread类的方式
public class Window2 extends Thread {
//使用static修饰 使ticket只有一份
private static int ticket = 100;
//private static Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized(Window2.class) {
if (ticket > 0) {
try {
Thread.sleep(60);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "您拿到了第" + ticket + "张票");
ticket--;
}else {
break;
}
}
}
}
}
class WindowTest2 {
public static void main(String[] args) {
Window2 thread1 = new Window2();
Window2 thread2 = new Window2();
Window2 thread3 = new Window2();
thread1.start();
thread2.start();
thread3.start();
System.out.println("--------------");
}
}
继承Thread类的例子用Window2.class本类充当同步监视器,即为Window2。如果我们将Window2.class换成this之后,每个线程对象进入后将自己作为同步监视器,三个线程的同步监视器都不同,此时仍然会发生重票和错票,线程不安全。
PS:在使用同步代码块时,特别需要注意的除同步监视器外,还需要注意同步代码块的范围,不能多,也不能少哦。上述栗子中,如果将同步代码块包住while循环将导致第一个进入的线程将所有票全部抢完。这显然不是我们希望看到的结果。
前文创建多线程的方式一(继承Thread类)中,我们曾说,共享的数据要使用static修饰,使其只有一份,此处的ticket即为共享,若去掉static,每个线程对象创建时都会有自己的ticket。最后自己拿到自己创建的无效票。实现Runnable接口的方式则无需担心此问题,Runnable接口实现类的对象只创建一次便可给多个Thread使用。
2、同步方法
说同步方法之前我们先带着一个问题,synchronized与static synchronized的区别。
如果操作共享数据的代码完整的声明在某个方法中,只要对这个方法使用synchronized关键字修饰
[public] [static] synchronized void method( ){ }
同步方法中看似没有同步监视器,实则同步方法中仍然涉及同步监视器,只是我们不用显示的对其进行声明。
(万年不变的买票例子!!!)
实现Runnable接口中的同步方法
public class Window3 implements Runnable {
private int ticket = 100;
//设置flag 当票买完时 为false结束循环
private boolean flag = true;
@Override
public void run() {
while (flag) {
buyTicket();
}
}
private synchronized void buyTicket(){
if (ticket > 0) {
try {
Thread.sleep(60);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "您拿到了第" + ticket + "张票");
ticket--;
} else {
flag = false;
}
}
}
class WindowTest3{
public static void main(String[] args) {
Window3 window3 = new Window3();
Thread thread1 = new Thread(window3,"甲");
Thread thread2 = new Thread(window3,"乙");
Thread thread3 = new Thread(window3,"丙");
thread1.start();
thread2.start();
thread3.start();
System.out.println("--------------");
}
}
继承Thread类中的同步方法
public class Window4 extends Thread {
private static int ticket = 100;
private static boolean flag = true;
@Override
public void run() {
while (flag) {
buyTicket();
}
}
private static synchronized void buyTicket(){
// private synchronized void buyTicket(){ 直接加synchronized 同步监视器为当前对象,每个对象进入时同步监视器都不同
if (ticket > 0) {
try {
Thread.sleep(60);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "您拿到了第" + ticket + "张票");
ticket--;
}else {
flag = false;
}
}
}
class WindowTest4{
public static void main(String[] args) {
Window4 thread1 = new Window4();
Window4 thread2 = new Window4();
Window4 thread3 = new Window4();
thread1.start();
thread2.start();
thread3.start();
System.out.println("--------------");
}
}
细心的朋友已经发现了,实现Runnable接口的方式的同步方法是synchronized修饰,继承Thread类的方式是static synchronized。
前文的同步代码块中Runnable接口的方式使用synchronized(this),Thread中则使用synchronized( Window2.class)。当我们把static synchronized中的static去掉发现,依然会发生从票和错票现象,线程不安全。
上述栗子中实现Runnable接口的方式,Runnable接口实现类的对象仍然只有一个,其余线程共用此对象实现多线程,同步方法直接使用synchronized修饰,此时的同步监视器使用即为this。而继承Thread类的方式则有多个对象,同步方法使用static synchronized修饰,同步监视器即为 XXX.class。
非静态同步方法(synchronized修饰),同步监视器是:this
静态同步方法(static synchronized修饰),同步监视器是:当前类本身
Lock解决线程安全问题
Lock(锁)解决线程安全问题的方式是JDK5.0新增的方式。
Lock接口的实现类有
ReentrantLock
ReetrantReadWriteLock.WriteLock
ReentrantReadWriteLock.ReadLock
此处用ReentrantLock举例
private static int ticket = 100;
private static boolean flag = true;
//创建锁对象
private static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (flag) {
//上锁
lock.lock();
try {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "您拿到了第" + ticket + "张票");
ticket--;
} else {
flag = false;
}
}finally {
//释放锁
lock.unlock();
}
}
}
需要注意的是:lock()必须紧跟try使用unlock() 必须在finally第一行调用。
阿里巴巴开发手册中对Lock使用的的描述
{在使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。
说明一:如果在lock方法与try代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。
说明二:如果lock方法在try代码块之内,可能由于其它方法抛出异常,导致在finally代码块中,unlock对未加锁的对象解锁,它会调用AQS的tryRelease方法(取决于具体实现类),抛出IllegalMonitorStateException异常。
说明三:在Lock对象的lock方法实现中可能抛出unchecked异常,产生的后果与说明二相同。}
四、线程的生命周期
线程的状态
·新建 当一个Thread类活其子类的对象被声明并创建时,新生的线程对象处于新建状态
·就绪 调用start() 后,将进入线程队列等待CPU时间片,具备了运行的条件,只是没分配到资源
·运行 当就绪的线程被调度,并获得CPU资源时,进入运行状态,run()定义了线程的操作和功能
·阻塞 在某些特殊情况下,被挂起或执行输入输出操作时,让出CPU资源
·死亡 线程完成了全部工作或线程被提前强制性的终止,或出现异常导致结束
五、多线程中常用方法
Thread中多线程的常用方法
1、start(); 启动当前线程,调用当前线程的run()方法。
2、run():执行多线程的代码;
3、getName():获取当前线程的名称;
4、setName(String str):设置线程的名称;
5、yield():线程执行此方法的时候,释放当前cpu执行权
6、join():在线程a中调用线程b的join(),此时线程a进入阻塞状态,直至线程b完全执行完以后,线程a才结束阻塞状态,个人理解为,操作系统中的抢占式调度。
7、sleep(long time):使当前线程阻塞 x ms(毫秒)
需要注意的是:sleep的异常不能在run()中抛出,Runnable接口中的run()方法并未抛出异常。 遵循子类重写规则。
Object中多线程的常用方法
wait(); 执行此方法,当前线程进入阻塞状态,并释放同步监视器。
notify();执行此方法,唤醒一个阻塞的线程,如果有多个wait()的线程,唤醒优先级的高的线程。
notifyAll();执行此方法,唤醒所有wait()的线程。
PS:(1)以上三个方法必须使用在同步代码块或同步方法中
(2)以上三个方法的调用者必须是同步代码块或同步方法中的同步监视器。 否则会出IllegalMonitorStateException异常
(3)以上三个方法的定义是在Object类中。WHY?
原因:因为三个方法的调用者是同步监视器,又因为任意对象都可以充当同步监视器, 故将这三个方法的定义放到Object中。
sleep();和wait();的异同
1、相同点:一旦执行,都可以使得当前线程进入阻塞状态。
2、不同点:1)两个方法声明的位置不同。 sleep()定义在Thread中 而。 2)调用的要求不同:sleep()可以在任何需要的场景中调用,wait()必须使用在同步代码块或同步方法中。3)关于释放同步监视器:如果sleep()在不会释放,wait()会释放。
线程的优先级
MAX_PRIORIYT 10
MIN_PRIORIYT 1
NORM_PRIORIYT 5
设置线程优先级
setPriority( int p)
说明:高优先级的线程才能够要抢占低优先级cpu的执行权,但是知识从概率上讲,高优先级的线程高概率的情况下被执行,并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。