基础——线程

牧云@^-^@ 提交于 2020-08-18 05:15:35

 

一、线程执行的内存原理

	public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

		t1.start();
        t2.start();
	}

对应的内存原理图大致是这样:

注意事项:

1.执行线程任务的run方法是线程私有的。
2.某个线程对象出现异常不会影响其他线程的执行。

二、创建线程的方式

(1)继承Thread类

1、步骤

1.定义一个类,继承Thread类。
2.重写Thread类的run方法。
3.创建线程对象。
4.调用start方法开启新线程,内部会执行run方法。

2、代码示例

public class MyThread extends Thread {
    @Override
    public void run() {
        // 获取线程名称
        String threadName = this.getName();
        for (int i=0;i<100;i++) {
        	// 复习异常抛出的方法,抛出一个运行时异常
            if("Thread-1".equals(threadName) && i == 3){
                throw new RuntimeException(threadName + "出问题了");
            }
            System.out.println(threadName+"..."+i);
        }
    }
}

3、Thread类的构造函数

  • public Thread():分配一个新的线程对象。
  • public Thread(String name):分配一个指定名字的新的线程对象。
  • public Thread(Runnable target):分配一个带有指定目标新的线程对象。
  • public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。

4、常用成员方法

  • public String getName():获取当前线程名称。
  • public String start();导致此线程开始执行;java虚拟机调用此线程的run方法。
  • public void run():定义线程的任务逻辑。

5、常用静态方法

  • public static void sleep(long millis): 让当前正在执行的进程暂停指定毫秒数。
  • public static Thread currentThread():返回对当前正在执行的线程对象的引用。
     

(2)实现Runnable接口
1、步骤

1.定义Runnable接口的实现类
2.实现类覆盖重写run方法,指定线程任务逻辑
3.创建Runnable接口实现类对象
4.创建Thread类对象,构造函数传递Runnable实践类对象。

a) public Thread(Runnable target):分配一个带有指定目标新的线程对象。
b) public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。

5.Thread类对象调用start方法,内部自动调用run方法。

2、代码展示

public class MyTask implements Runnable{
    @Override
    public void run() {
        //任务逻辑
    }
}
	MyTask myTask = new MyTask();
    Thread thread = new Thread(myTask);
    thread.start();

2、(重点!)实现Runnable接口来创建线程的好处

1.避免java中类单继承的局限性
2.降低线程任务对象和线程之间的耦合性

tip:换句话说,我们可以更加专注于线程的任务,先把线程的任务逻辑创建完毕。之后需要执行线程任务的地方就创建线程,执行需要的线程任务即可。并且线程任务可以多次反复使用。有点像零件插拔一样。

(3)匿名内部类方式

1、格式

new 父类/接口(){
	//覆盖重写抽象方法
};

2、作用

1.创建父类子类对象的快捷方式
2.创建接口的实现类对象的快捷方式

3、注意事项

1.使用匿名内部类创建的对象只能一次性使用
2.尽量使用lambda表达式进行书写,提高代码可读性和编程效率。

三、(重点!)线程安全问题

(1)出现线程安全的情况

1.有两个以上线程同时操作共享数据

2.操作共享数据的语句有两条以上

3.线程调度是抢占式调度模式。

(2)解决方案

  • 同一个线程,操作共享数据的多条语句全部执行,要么多条语句全部不执行。故而可以使用同步技术。
  • 同步的原理:有锁的线程执行,没有锁的线程等待。

(3)实际解决

1、同步代码块

1.作用:用来解决多线程访问共享数据安全问题

2.格式

synchronized(任意对象){

}
  1. 注意事项
    (1)所有操作共享数据的代码写到同步代码块{}中。
    (2)任意对象:任意指的是类型可以任意,但要保证全局唯一,被多个线程共享使用
    1. 任意对象,也叫锁对象。更加专业的术语:对象监视器。

2、同步方法

  1. 格式
修饰符 synchronized 返回值类型 方法名称(参数列表...){
	...
}
修饰符 synchronized 返回值类型 方法名称(参数列表...){
	...
}

注意事项

(1)所有操作共享数据的代码都在{}中间添加一个
(2)同步方法的锁对象就是this

3、使用Lock接口

1.方法:
1.abstract void lock​() 获得锁。
2.abstract void unlock​() 释放锁。

2.实现类:

java.util.concurrent.locks.ReentrantLock ,空参构造函数

3.注意事项

释放锁的动作必须被执行。

(4)实际案例

1、卖票案例分析

	(1)总共有3种途径卖票,每个途径,相当于一个线程对象
    (2)每个线程对象要执行的任务: 都是在卖票
    (3)3个线程对象,操作的资源 100 张票 是被共享的

2、解决策略:

(1) 定义实现类,实现Runnable接口

(2) 覆盖重写Runnable接口中的run方法.指定线程任务——卖票
(2.1)判断是否有票
(2.2)有: 出一张票
(2.3)票的数量减少1

(3) 创建Runnable接口的实现类对象

(4) 创建3个Thread对象,传递Runnable接口的实现类对象,代表,卖票的3种途径

(5) 3个Thread对象分别调用start方法,开启售票

3、代码实现

public class MyTicket implements Runnable{
    private int tickets= 100;
    Object obj = new Object();
    @Override
    public void run() {
        while (true){
//            sellTicketB();
            sellTicketA();
        }
    }

	// 同步函数
    private synchronized void sellTicketA(){
        if(tickets>0){
            System.out.println(Thread.currentThread().getName() + " 卖出第" + tickets-- + "张票");
        }else {
            return;
        }
    }
	
	//同步进程快
    private void sellTicketB() {
        synchronized(obj){
            if(tickets>0){
                System.out.println(Thread.currentThread().getName() + " 卖出第" + tickets-- + "张票");
            }else {
                return;
            }
        }
    }
}
public class synchronizedTest {
    public static void main(String[] args) {
        MyTicket task = new MyTicket();
		// 三个线程任务来出票
        new Thread(task).start();
        new Thread(task).start();
        new Thread(task).start();
    }
}

4、线程同步的原理

  1. 线程执行的前提:
    (1)cpu资源
    (2)锁对象

  2. 基本规则:
    线程对象执行同步代码块中的内容,要么全部执行,要么全部不执行,不能够被其他线程干扰。

  3. 拿买票案例举例说明

现在存在t0、t1和t2三个线程。

假设一:
假设t0线程获取cpu资源,执行线程任务遇到同步代码块,判断是否具有锁对象。
有:获取锁对象
进入同步代码块,执行同步代码,,假设t0在执行过程中没有被t1或者t2抢夺cpu资源,那么t0或顺利执行完同步代码块内代码,退出同步代码块,释放锁资源,继续和其他线程抢夺cpu资源和锁对象。

假设二:
假设t0线程获取cpu资源,执行线程任务遇到同步代码块,判断是否具有锁对象
有:获取锁对象
进入同步代码块,执行同步代码,假设t0在执行过程中被t1抢夺了cpu资源,那么t0线程将不能继续执行。t1线程执行任务,遇到同步代码块,判断是否具有锁对象,因为锁已经被t0拿了,因此t1进入阻塞状态,等待获取锁对象被释放。

假设三:
假设t0执行完成了同步代码块的内容,释放了锁对象,t1处于阻塞状态,但此时t2线程抢到了cpu资源,执行代码到同步代码块,然后顺利获取锁对象,进入同步代码块执行。这种情况下,t1将继续等待t2在同步代码块执行完毕,然后再去抢夺cpu资源和锁资源。

可以发现,线程如果不进行调度的管理可能会出现长时间等待的问题,因为抢占式调度具有随机性,不能获得最大的性能。

(持续更新…)

四、线程状态

java.lang.Thread.State给出了六种线程状态

注意事项(一)

1.sleep方法可以在同步中使用
2.sleep方法可以在非同步中使用
3.sleep方法与锁对象无关(不会释放锁)

注意事项(二)

1.Object类定义wait和notify方法
2.因此任意对象可以调用wait()和notify()方法
3.锁对象可以是任意的
4.但是锁对象必须使用在同步中,因此wait和notify方法必须在同步中使用

案例分析

双线程交替执行。有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为 
{10,5,20,50,100,200,500,800,2,80,300,3000}; 
创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”,随机从抽奖池中完成抽奖。

两个线程轮流交替抽奖,每抽出一个奖项就打印出来。
【输出示例】
	抽奖箱1...抽出了10元...
	抽奖箱2...抽出了20元...
	抽奖箱1...抽出了50元...
	抽奖箱2...抽出了800元...
	... ...
每次抽的过程中,不打印,抽完时一次性打印。
【输出示例】
	在此次抽奖过程中,抽奖箱1总共产生了6个奖项,分别为:10,5,20,50,100,200最高奖项为200元,总计额为385元
	在此次抽奖过程中,抽奖箱2总共产生了6个奖项,分别为:500,800,2,80,300,3000最高奖项为3000元,总计额为4682元
	在此次抽奖过程中,抽奖项2中产生了最高奖项,该最高奖项为3000元

1. 分析

两个线程的任务都是抽奖,因此很明显只需要定义一个线程任务对象“抽奖”即可。由于两个抽奖箱共享一个奖池,且要求两个抽奖箱交替进行,很明显需要用到线程的等待(wait)和唤醒(notify)操作。因此,两个线程对于奖池中奖金的操作需要同步。前面已经说明,线程任务只有一个,故同步代码块的锁对象使用线程任务对象自身(this)即可。

2、实现思路

 

3、代码实现

public class RunnableImpl implements Runnable{
    private List list;
    private Map<String,List> mp = new HashMap<>();
    private int count = 0;


    public RunnableImpl(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        List subList = new ArrayList();
        while (true){
            synchronized (this){
                if(list.size()<=0){
                    this.notifyAll();
                    mp.put(Thread.currentThread().getName(),subList);
                    count++;
                    if(count == 2){
                        Integer max = 0;
                        String max_name = "";
                        for (Map.Entry<String, List> entry : mp.entrySet()) {
                            List t = entry.getValue();
                            String s = entry.getKey();
                            Integer tmax = 0;
                            Integer sum = 0;
                            StringBuilder sb = new StringBuilder();
                            for (Object o : t) {
                                sb = sb.append(o).append(",");
                                sum += (Integer)o;
                                tmax = tmax < (Integer)o ? (Integer)o : tmax;
                            }
                           if(max < tmax){
                               max = tmax;
                               max_name = s;
                           }
                            //sb = sb.deleteCharAt(sb.length()-1);
                            String seq =sb.toString();
                            System.out.println("在此次抽奖过程中,"+ s +"总共产生了"+t.size()+"个奖项,分别为:" + seq +"最高奖项为"+ tmax +"元,总计额为"+ sum +"元");
                        }
                        System.out.println("在此次抽奖过程中,"+max_name+"中产生了最高奖项,该最高奖项为"+max+"元");
                    }
                    break;
                }
                if(list.size() > 0){
                    Object remove = list.remove(new Random().nextInt(list.size()));
                    System.out.println(Thread.currentThread().getName() + "..." + "抽出了"+ remove +"元...");
                    subList.add(remove);

                    this.notify();
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!