Java---多线程基础总结

Java基础—多线程

一、基本概念

1.程序:指为完成特定任务编写的一组指令的集合。
2.进程:程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:自身的产生–存在–消亡—生命周期
3.线程:指程序内部的一条执行路径;若一个进程同一时间并行执行多个线程,就是支持多线程的(多个线程共享内存区域的方法区和堆);线程是调度和执行的最小单位,每个线程拥有独立的运行栈和程序计数器(pc)。
4.并行和并发:
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀。

1.多线程的优点:

1.提高应用程序的响应。
2.提高计算机系统CPU的利用率
3.改善程序结构。将即长又复杂的进程分为多个线程;独立运行,利于理解和修改。

2.何时需要多线程

1.程序需要同时执行两个或多个任务。
2.程序需要实现一些需要等待的任务时,如用户输入、文件读写、网络操作、搜索等。
3.需要一些后台运行的程序时。

二、线程的创建和使用

通过java.lang.Thread类实现

1.多线程的创建方式一:

继承于Thread类(步骤):
1.创建一个继承于Thread类和子类。
2.重写Thread类的run()---->将此线程执行的操作声明在run方法中。
3.创建Thread类的子类的对象。
4.通过此对象调用start()。

例子:遍历100以内的所有偶数

package com.TestThread.java;
//    1.创建一个继承于Thread类和子类
class MyThread extends Thread{
//    2.重写Thread类的run()---->将此线程执行的操作声明在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) {
        //    3.创建Thread类的子类的对象
        MyThread t1= new MyThread();
        //    4.通过此对象调用start()
        t1.start();//
    }
}

start的作用:
1.启动当前线程。
2.调用当前线程的run()。

Thread类的api

void start():启动线程,并执行对象的run()方法
run():线程在被调度时执行的操作
String getName():返回线程的名称
void setName(String name):设置该线程名称
static Thread currentThread():返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
static void yield():线程让步
1.暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
2.若队列中没有相同优先级,忽略此方法
join():当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止
static void sleep(long mills):一旦执行使得线程进入阻塞状态。sleep()不会释放锁。

Java的调度:同优先级线程组成先进先出队列(先到先服务),使用时间片策略;高优先级,使用优先调度的抢占式策略

2.多线程的创建方式二:

实现Runnable接口(步骤):
1.创建一个实现Runnable接口的类
2.实现类去实现Runnable中的抽象方法:run()
3.创建实现类的对象
4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5.通过Thread类的对象调用start():① 启动线程 ② 调用当前线程的run()—>调用Runnable类型的target

//    1.创建一个实现Runnable接口的类
class MyThread implements Runnable{
    //    2.实现类去实现Runnable中的抽象方法: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) {
        //    3.创建实现类的对象
        MyThread mThread = new MyThread();
        //    4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1= new Thread(mThread);
        //    5.通过Thread类的对象调用start():① 启动线程 ② 调用当前线程的run()--->调用Runnable类型的target
        t1.start();
    }
}

创建线程两种方式的对比
开发中优先选择:实现Runnable接口的方式
原因
1.实现的方式没有类的单继承性的局限性
2.实现的方式更适合来处理多个线程有共享数据的情况
联系:public class Thread implement Runnable
相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。

案例:(方式一实现)

package TestThread.java;
/**
* 例子:创建三个窗口卖票,总票数为100张.使用继承Thread类的方式
* 存在线程的安全问题,待解决。
*/
class Window extends Thread{
    private static int ticket = 100;
    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}
public class WindowTest {
    public static void main(String[] args) {
        Window t1 = new Window();
        Window t2 = new Window();
        Window t3 = new Window();
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

(方式二实现):

package TestThread.java;
/**
* 例子:创建三个窗口卖票,总票数为100张.使用实现Runnable接口的方式。
* 存在线程的安全问题,待解决。
*/
class Window1 implements Runnable{
    private int ticket = 100;
    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}
public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

三、线程的生命周期

线程生命周期图

四、线程同步(主要解决线程的安全问题)

1.问题:卖票过程中出现了重票、错票------->出现了线程安全问题
2.问题出现的原因:当某个线程操作车票的过程中,尚未完成时,其他线程参与进来,也操作车票。
3.如何解决:当一个线程在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
4.Java中,通过同步机制,来解决线程的安全问题。

解决线程安全问题的方式一:同步代码块
    synchronized(同步监视器){
            //需要被同步的代码
}

说明:1.操作共享数据的代码,即为需要被同步的代码。---->不能包含代码多了,也不能包含代码少了。
共享数据:多个线程共同操作的变量。(只有有了共享数据,才会存在线程安全问题)。比如:ticket就是共享数据。
2.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。Object obj = new Object();//不能放到run()函数里面,放到外面。
要求:多个线程必须要用同一把锁,一定要唯一。设置对象唯一时,可以把其设置为static,设为共享的
**补充:**在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。在继承Tread类创建多线程的方式中,慎用this来从当同步监视器。可以考虑使用当前类,来充当同步监视器

解决线程安全问题的方式二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明为同步(synchroniezd)的。
关于同步方法的总结:
1.同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
2.非静态的同步方法:(使用同步方法处理实现Runnable接口方式中的线程安全问题),同步监视器:this

静态的同步方法:(使用同步方法处理继承Thread类的方式中的线程安全问题),同步监视器:当前类 window.class
3.同步方式。解决了线程安全的问题。-------好处
操作同步代码时,只能有一个线程参与。相当于一个单线程的过程,效率低。-------局限性

class Window1 implements Runnable{
    private int ticket = 100;
//    Object obj = new Object();
//    Dog dog = new Dog();
    @Override
    public void run() {
//        Object obj = new Object();
        while(true){
            synchronized (this){//此时的this:唯一的Window1的对象   //方式二:synchronized (dog) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}
public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w = new Window1();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Dog{
}
线程的死锁问题:

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所以的线程都处于阻塞状态,无法继续。
解决的办法:
1.专门的算法、原则
2.尽量减少同步资源的定义
3.尽量避免嵌套同步

解决线程安全问题的方式三:LOCK(锁) (重要)

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁。

package com.TestLock.java1;
import java.util.concurrent.locks.ReentrantLock;
/**
* 解决线程安全问题的方式三:Lock锁  --- JDK5.0新增
*/
class Window implements Runnable{
   private int ticket = 100;
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while(true){
            try{
                //2.调用锁定方法lock()
                lock.lock();
                if(ticket > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }finally {
                //3.调用解锁方法:unlock()
                lock.unlock();
            }
        }
    }
}
public class LockTest {
    public static void main(String[] args) {
        Window w = new Window();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

面试题:synchronized 与 Lock的异同?
相同:二者都可以解决线程安全问题
不同synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器。 Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())

优先使用顺序:Lock —>同步代码块(已经进入了方法体,分配了相应资源)—>同步方法(在方法体之外)

五、线程的通信

线程通信的例子:使用两个线程打印 1-100。线程1, 线程2 交替打印。
涉及到的三个方法:
wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
说明:
1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
否则,会出现IllegalMonitorStateException异常
3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。

**面试题:**sleep()和wait()的异同?
1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁

**sleep 方法:**是属于 Thread 类中的,sleep 过程中线程不会释放锁,只会阻塞线程,让出cpu给其他线程,但是他的监控状态依然保持着,当指定的时间到了又会自动恢复运行状态,可中断,sleep 给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会

**yield和sleep的区别:**和 sleep 一样都是 Thread 类的方法,都是暂停当前正在执行的线程对象,不会释放资源锁,和 sleep 不同的是 yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。还有一点和 sleep 不同的是 yield 方法只能使同优先级或更高优先级的线程有执行的机会

小结:释放锁的操作
1.当前线程的同步方法、同步代码块执行结束
2.当前线程同步代码块、同步方法中遇到break、return终止了该代码块,该方法继续执行
3.当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
4.当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁

不会释放锁的操作:
1.线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
2.线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。应尽量避免使用suspend()和resume()来控制线程。

class Number implements Runnable{
    private int number = 1;
    private Object obj = new Object();
    @Override
    public void run() {
        while(true){
            synchronized (obj) {
                obj.notify();
                if(number <= 100){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;
                    try {
                        //使得调用如下wait()方法的线程进入阻塞状态
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }
    }
}
public class CommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);
        t1.setName("线程1");
        t2.setName("线程2");
        t1.start();
        t2.start();
    }
}

六、JDK5.0新增线程创建方式

创建多线程的方式三:实现Callable接口

创建线程的方式三:实现Callable接口。 — JDK 5.0新增
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
1. call()可以有返回值的。
2. call()可以抛出异常,被外面的操作捕获,获取异常的信息
3. Callable是支持泛型的
步骤:
1.创建一个实现Callable的实现类
2.实现call方法,将此线程需要执行的操作声明在call()中
3.创建Callable接口实现类的对象
4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
6.获取Callable中call方法的返回值
实现代码:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
*/
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}
public class ThreadNew {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();
        try {
            //6.获取Callable中call方法的返回值
            //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
创建多线程的方式四:使用线程池(开发中真正使用的方式)

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似于生活中的公共交通工具。
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
步骤:
1. 提供指定线程数量的线程池
2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象。(告诉线程是要干什么)
3.关闭连接池

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 NumberThread1 implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}
public class ThreadPool {
    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //设置线程池的属性
//        System.out.println(service.getClass());
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();
        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象。(告诉线程是要干什么)
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable
//        service.submit(Callable callable);//适合使用于Callable
        //3.关闭连接池
        service.shutdown();
    }
}
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值