Java 多线程与线程安全基础

进程与线程

  1. 进程: 指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以有多个线程.
  2. 线程: 指进程中的一个执行任务(控制单元),是程序执行时的最小单位,是CPU调度和分派的基本单位,一个进程可以运行多个线程,多个线程可共享资源.

并行与并发

  1. 并行: 多个进程同时运行,发生在多CPU中,一个进程占一个CPU,所有进程同步运行,互相之间不抢夺资源
  2. 并发: 多个进程间隔运行,发生在单CPU中,每个进程切换运行,切换时间很短,所以看起来好像是同时在运行,互相之间抢夺资源

同步与异步

  1. 同步: 多个线程有序执行,前面的执行完了后面的补上,在前面线程运行时其他线程都在等待
  2. 异步: 多个线程同时进入就绪状态,等待CPU的统一调度,当线程进入运行状态时其他线程可以访问其他资源,把访问资源的空闲时间利用起来,能提高效率,也就是多线程机制

线程分类

  1. 应用线程: 前台线程,执行各种具体任务的线程,一个程序启动至少有一个应用线程(main线程)和一个守护线程(GC)
  2. 守护线程: 后台线程,随着前台线程的全部死亡,守护线程也会自动死亡

线程的创建和启动

继承方式创建
  1. 自定义类继承Thread
  2. 重写run方法,把线程任务封装进run方法中
  3. 创建自定义对象
  4. 自定义对象调用start()方法,开启线程
// 继承方式创建线程
public class MyThreadDemo extends Thread {  // 继承Thread类创建线程
    // 重写run方法,把线程任务封装进run方法
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("吃了" + (i + 1) + "顿");
        }
    }
}
// 开启线程
public class MyThreadTestDemo {
    public static void main(String[] args) {
        // 创建线程对象,可以使用多态的方法创建, Thread m = new MyThreadDemo();
        MyThreadDemo m = new MyThreadDemo();
        // 开启线程
        m.start();
        // 线程开启后进入独立模块,CPU调度随机,也就是说两个线程随机运行
        for (int i = 0; i < 10; i++) {
            System.out.println("睡了" + (i + 1) + "天");
        }
    }
}
接口实现方式创建
  1. 自定义类实现Runnable接口
  2. 重写run方法,把线程任务封装进run方法
  3. 创建自定义对象
  4. 创建Thread类对象,将自定义对象作为参数传入Thread类创建对象的构造器
  5. Thread对象调用start()方法,开启线程
// 实现Runnable接口创建线程
public class MyRunnable implements Runnable{    // 实现Runnable接口创建线程
    // 重写run方法,将线程任务封装进run方法
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("吃了" + (i + 1) + "顿");
        }
    }
}
// 启动线程
public class MyRunnableDemo {
    public static void main(String[] args) {
        // 创建线程对象
        MyRunnable m = new MyRunnable();
        // 创建Thread类对象,将线程对象作为构造器参数传过去
        Thread t = new Thread(m);
        // 开启线程
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("睡了" + (i + 1) + "天");
        }
    }
}

线程的生命周期

  1. 当程序创建一个线程以后,线程处于新建状态,无法被CPU调度,需要调用start()方法开启线程,调用start()方法之后线程进入就绪状态,等待CPU调度
  2. 当就绪状态的线程被CPU调度时进入运行状态,运行状态和就绪状态可以相互切换,当多个线程切换速度很快时,我们看起来就像多个线程在同步运行,这就是并发
  3. 运行状态可以切换到等待状态,它会等待另一个线程来执行一个任务,一个等待状态的线程只有通过另一个线程通知它转到可运行状态,才能继续执行
  4. 运行状态转到等待状态可以设置一个计时器,等待特定的时间之后唤醒线程对象
  5. 运行状态遇到异常或者其他特殊状况导致不能运行时进入阻塞状态,让出CPU调度,停止自身运行
  6. 当线程执行完或者抛出未捕获的异常和错误时,或者调用线程的stop()方法,线程终止,生命周期结束

操作线程的方法

  1. join方法: 主要作用是同步,它可以使得线程之间的并行执行变为串行执行.比如在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行
  2. sleep方法: 让正在执行的线程暂停一段时间,进入阻塞状态.
// sleep(long milllis) throws InterruptedException; 毫秒为单位
Thread.sleep(1000);	// 需要处理异常,使用try-catch
  1. 线程的优先级: 每个线程都有优先级,优先级的高低只和线程获得执行机会的次数多少有关,并不是说优先级高的就一定先执行,哪个线程的先运行取决于CPU的调度.Thread对象的setPriority(int x)和getPriority()用来设置和获得优先级,x的值不能大于线程执行的次数,否则报IllegalThreadStateException异常
  2. 后台线程: 守护线程,一般用于为其他线程提供服务,若前台线程都死亡,后台线程自动死亡.Thread对象setDaemon(true)用来设置后台线程,setDaemon(true)必须在start()调用前,否则抛出IllegalThreadStateException异常

线程安全性: 当多线程并发访问同一资源时会导致线程出现安全性的原因

需求: 现有100个苹果,现在有3个人去吃.

继承方式
// 线程类,继承Thread类
public class SafeThread extends Thread {
    private int number = 100;   // 苹果数量

    public SafeThread(String name) {	// 构造器,调用父类构造器把值传给父类
        super(name);
    }
 
 	// 重写run方法,把线程任务封装进去
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName()
                + "吃了第" + number-- + "个苹果");
            }
        }
    }

}

// 测试类
public class SafeThreadDemo {
    public static void main(String[] args) {
    	// 创建对象
        Thread s1 = new SafeThread("xx");
        Thread s2 = new SafeThread("yy");
        Thread s3 = new SafeThread("zz");
        // 开启线程
        s1.start();
        s2.start();
        s3.start();
    }
}
// 结果是3个人每个人都吃了100个苹果,继承方式多个线程不能共享同一资源
实现接口方式
// 创建线程类实现Runnable接口
public class SafeRunnable implements Runnable {
    private int number = 100;   // 苹果数量

    // 重写run方法,把线程任务封装进去
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (number > 0) {
                System.out.println(Thread.currentThread().getName()
                        + "吃了第" + number-- + "个苹果");
            }
        }
    }

}

// 测试类
public class SafeRunnableDemo {
    public static void main(String[] args) {
        // 创建线程对象
        SafeRunnable s = new SafeRunnable();
        // 创建Thread对象把线程对象当作参数参数传过去,开启线程
        new Thread(s,"xx").start();
        new Thread(s,"yy").start();
        new Thread(s,"zz").start();
    }
}
// 结果是3个人平分了100个苹果,实现方式可以多个线程共享同一资源
继承方式与实现方式的区别
  1. Java中类是单继承,如果继承了Thread类就不能再继承其他类了,而实现方式不但可以再继承其他类,还可以实现多个接口,所以实现方法比较好用
  2. 继承方式多个线程没法共享同一资源,实现方式可以做到多个线程共享同一资源

线程同步

当多线程访问同一资源对象的时候可能会出现线程不安全的问题,实现Runnable接口创建线程的时候看起来可能没有问题,但是出现网络延迟的时候就会出现,这里用线程睡眠来模拟网络延迟

// 添加线程睡眠
public class SafeRunnable implements Runnable {
    private int number = 100;   // 苹果数量

    // 重写run方法,把线程任务封装进去
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (number > 0) {
                try {
                    Thread.sleep(100);	// 让线程休眠0.1秒再运行,相当于网络出现0.1秒延迟
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()
                        + "吃了第" + number-- + "个苹果");
            }
        }
    }
}

// 测试类
public class SafeRunnableDemo {
    public static void main(String[] args) {
        // 创建线程对象
        SafeRunnable s = new SafeRunnable();
        // 创建Thread对象把线程对象当作参数参数传过去,开启线程
        new Thread(s,"xx").start();
        new Thread(s,"yy").start();
        new Thread(s,"zz").start();
    }
}

测试结果
在这里插入图片描述
解决思路:在一个线程执行该任务的时候其他的线程不能来打扰,也就是设置一个同步锁,比如说A线程进入同步锁进行操作的时候,B和C以及其他的线程只能在外面等着,A操作结束,释放同步锁,A,B,C以及其他线程才会有机会去抢同步锁(A线程只要没有执行完所有任务,同样进入争夺同步锁的行列,谁获得同步锁,谁才能执行代码)

同步代码块

同步锁:

  1. 对于非static方法,同步锁就是this
  2. 对于static方法,同步锁就是当前方法所在类的字节码对象(.class文件)
public class SafeRunnable implements Runnable {
    private int number = 100;   // 苹果数量

    // 重写run方法,把线程任务封装进去
    public void run() {
        for (int i = 0; i < 100; i++) {
        	/*
        		加同步锁
        		java允许使用任何对象作为同步监听对象
        		一般我们把当前并发访问的共同资源作为同步监听对象
        		这里的SafeRunnable对象拥有三个线程共同的资源,
        		而且只有一份,所以可以用来做同步锁
        		
        	*/
            synchronized (this) {	// 加同步锁
                if (number > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()
                            + "吃了第" + number-- + "个苹果");
                }
            }
        }
    }

}
同步方法
public class SafeRunnable implements Runnable {
    private int number = 100;   // 苹果数量

    // 重写run方法,把线程任务封装进去
    public void run() {
        for (int i = 0; i < 100; i++) {
            doWork();
        }
    }

    // synchronized修饰的方法是同步方法
    synchronized public void doWork() {
        if (number > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()
                    + "吃了第" + number-- + "个苹果");
        }
    }
}
synchronized的优劣势
  1. 好处: 保证了多线程并发访问时的同步操作,避免线程的安全性问题
  2. 缺点: 使用synchronized的方法/代码块的性能要低一些
  3. 建议: 尽量减小synchronized的作用域
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值