多线程基础学习

66 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情

多线程基础学习

普通方法调用和多线程方法

执行run方法,只有主线程一条路径执行

执行start方法,则是主线程和子线程并行交替执行

多线程调用

【注意】run方法就和普通的成员方法一样,而start方法则使用启动一个线程,使线程处于就绪状态

创建线程方法

继承Thread类

public class ExtendsThreadTest extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("继承Thread实现多线程" + i);
        }

    }

    public static void main(String[] args) {
        ExtendsThreadTest test = new ExtendsThreadTest();
        test.start();
        for (int i = 0; i < 200; i++) {
            System.out.println("多线程测试" + i);
        }
    }
}

实现Runnable接口

public class ImplRunnableTest implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("继承Thread实现多线程" + i);
        }
    }
    public static void main(String[] args) {
        // 创建Runnable接口的实现类对象
        ImplRunnableTest test = new ImplRunnableTest();

        // 创建一个线程对象,通过线程对象来开启我们的线程,这里使用了代理模式
        new Thread(test).start();

        for (int i = 0; i < 200; i++) {
            System.out.println("多线程测试" + i);
        }
    }
}

实现Callable接口(了解)

1、实现Callable接口,需要返回值类型

2、 重写call方法,需要抛出异常

3、创建目标对象

4、创建执行任务:ExcutorService ser Excutors.newFixedThreadPool()

5、提交执行:Future result1 = ser.submit(t1)

6、获取结果:Boolean r1 =result1.get()

7、关闭服务:ser.shutdownNow()

public class ImplCallableTest implements Callable<Boolean> {
    // 返回值同Callable接口的泛型一致
    @Override
    public Boolean call() throws Exception {
        System.out.println("实现callable接口创建线程方法成功");
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ImplCallableTest test = new ImplCallableTest();

        // 创建执行服务
        ExecutorService service = Executors.newCachedThreadPool();
        // 提交执行
        Future<Boolean> result = service.submit(test);
        // 获取结果
        Boolean r1 = result.get();
        System.out.println(r1);
        // 关闭服务
        service.shutdown();
    }
}

静态代理模式

真实类和代理类要实现同一个接口,并且在代理类中要实例化一个真实类对象

代理模式不同于工厂模式,代理类中可以进行扩展,工厂模式知识返回一个实例对象

// 接口
public interface Rent {
    void rent();
}

// 真实类
public class FangDong implements Rent {
    @Override
    public void rent() {
        System.out.println("房东出租房屋");
    }
}

// 代理类
public class StaticProxy implements Rent {
    private FangDong fangDong = new FangDong();
    @Override
    public void rent() {
        before();
        this.fangDong.rent();
        rentMoney();
    }

    public void rentMoney() {
        System.out.println("代理收中介费");
    }
    public void before() {
        System.out.println("代理帮忙出租房屋");
    }
}


// 使用代理类
public class Customer {
    public static void main(String[] args) {
        StaticProxy staticProxy = new StaticProxy();
        staticProxy.rent();
    }
}

JDK动态代理

动态代理实际上是通过传递参数来确定生成哪一个类的代理,它的实现需要两个重要的类

InvocationHandler:调用处理程序

Proxy

// 接口
public interface Rent {
    void rent();
}

// 真实类
public class Host implements Rent {
    @Override
    public void rent() {
        System.out.println("房东租房");
    }
}

// 代理类
public class ProxyInvocationHandler implements InvocationHandler {
    private Rent rent;

    public void setRent(Rent rent) {
        this.rent = rent;
    }

    public Object getProxy() {
        /**
         * public static Object newProxyInstance(ClassLoader loader,
         *                                        Class<?>[] interfaces,
         *                                        InvocationHandler h)
         * 该方法的三个参数:
         * loader:一个classloader对象,定义了由哪个classloader对象对生成的代理类进行加载
         * interfaces:一个interface对象数组,表示我们将要给我们的代理对象提供一组什么样的接口,如果我们提供了这样一个接口对象数组,那么也就是声明了代理类实现了这些接口,代理类就可以调用接口中声明的所有方法。
         * h:一个InvocationHandler对象,表示的是当动态代理对象调用方法的时候会关联到哪一个InvocationHandler对象上,并最终由其调用
         */
        return Proxy.newProxyInstance(this.getClass().getClassLoader(),rent.getClass().getInterfaces(),this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        seeHouse();
        Object result = method.invoke(rent, args);
        fare();
        return result;
    }

    public void seeHouse() {
        System.out.println("代理带领租客看房");
    }

    public void fare() {
        System.out.println("代理收押金");
    }
}

// 使用代理类
public class Client {
    public static void main(String[] args) {
        // 真实角色
        Host host = new Host();
        // 代理实例的调用处理程序
        ProxyInvocationHandler pih = new ProxyInvocationHandler();
        // 将真实角色类放置进去,说明此调用处理程序为该角色的代理实例的调用处理程序
        // 这里传入的角色不同,生成的代理类也不同
        pih.setRent(host);
        // 动态生成代理类
        Rent proxy = (Rent) pih.getProxy();
        proxy.rent();
    }
}

线程状态

五大状态

线程状态

线程方法

线程方法

停止线程

1、不推荐使用JDK提供的stop()、destroy()方法

2、推荐线程自己停下来,利用次数,不建议循环

3、建议使用一个标志位进行终止变量,当flag=false时,终止线程

线程休眠

1、sleep(时间)指定当前线程休眠的毫秒数

2、sleep存在异常InterruptedException

3、sleep时间达到后线程进入就绪态

4、sleep可以模拟网络时延,倒计时等

5、每一个对象都有一个锁,sleep不会释放锁

线程礼让

1、礼让线程,让当前正在执行的程序暂停,但不阻塞

2、线程由运行态进入就绪态

3、让CPU重新进行调度

线程强制执行(插队)

1、Join合并线程,待此线程执行完成后,在执行其他线程,其他线程阻塞

线程优先级

守护(daemon)线程

1、线程分为用户线程和守护线程

2、虚拟机必须确保用户线程执行完毕

3、虚拟机不用等待守护线程执行完毕

4、如后台记录操作日志、监控内存、垃圾回收等待,均为守护线程

同步

同步方法

synchronized关键字机制,包括两种用法:synchronized方法和synchronized块

同步方法:public synchronized void method(int args){}

synchronized方法控制对“对象”的访问,每个对象都对应一把锁,每个synchronized方法都必须获得调用方法的对象的锁才能执行,否则会阻塞进程,方法一旦执行,就独占该锁,直到该方法返回才能释放锁,后面被阻塞的线程才能获得这个锁,继续执行

缺陷:若将一个大的方法声明为synchronized将会影响效率

同步块

同步块:synchronized(Obj){}

Obj称之为同步监视器,就是被锁的公共资源(要修改的量)

- Obj可以是任何对象,但是推荐使用**共享资源**作为同步监视器
- 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class

同步监视器的执行过程

1. 第一个线程访问,锁定同步监视器,执行其中代码
2. 第二个线程访问,发现同步监视器被锁定,无法访问
3. 第一个线程访问完毕,解锁同步监视器
4. 第二个线程访问,发现同步监视器没有锁,然后锁定并访问

死锁

A同步块1中包含同步块2,同步块2需要B中占有的资源

B同步块3中包含同步块4,同步块4需要A中占有的资源

这就形成死锁

解决方法:将同步块2和4移动到同步块外

死锁条件:

互斥条件

请求并保持条件

不剥夺条件

循环等待条件

Lock锁

显式定义同步锁对象来实现同步,类似于PV原语

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象

ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁,释放锁

代码实现

// 注意使用lock锁的try-catch-finally的格式
class LockThread implements Runnable {
    private int ticket = 10;

    // Lock锁,可重入锁
    private final ReentrantLock reentrantLock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                // 显式加锁
                reentrantLock.lock();
                if (ticket > 0) {
                    System.out.println(ticket--);
                } else {
                    break;
                }
            } catch (Exception e) {
                System.out.println(e);
            } finally {
                // 显式释放锁
                reentrantLock.unlock();
            }
        }
    }
}

和synchronized比较

  1. Lock是显式锁(手动开启和关闭),synchronized是隐式锁,出了作用域自动释放
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)
  4. 优先使用顺序
    1. Lock > 同步代码块(已经进入了方法体,分配了相应资源,分配了相应资源)> 同步方法(在方法体之外)

线程通信

线程通信方法

线程池

ExecutorService

真正的线程池接口,常见子类ThreadPoolExecutor

  1. void execute(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
  2. Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
  3. void shutdown():关闭连接池

Executors

工具类、线程池的工厂类,用于创建并返回不同类型的线程池

最后

互联网寒冬虽寒,但永远缺真正的高级程序员,卷吧。