Java基础四:多线程

程序、进程、线程

  • 程序:一段静态代码
  • 进程:程序的一次执行过程,正在运行的一个程序。是资源分配的基本单位
  • 线程:一个进程可分为多个线程。线程是调度和执行的基本单位。每个线程拥有独立的运行栈和程序计数器。线程之间切换开销小。
  • 一个进程中的多个线程共享相同的内存单元/内存地址空间。他们从堆中共享同一对象,可以访问相同的变量和对象。这使得线程间通信更简便高效。但是也会带来进程同步的安全隐患
  • 内存解析:
    • 虚拟机栈、程序计数器是每个线程一份
    • 方法区、堆是每个进程一份

多线程的创建

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

//1.创建一个继承于Thread类的子类
class MyThread extends Thread{
	//2.重写Thread类的run()
	@Override
	public void run(){
		//方法体
		}
	}
}

public class ThreadTest {
	public static void main(String[] args) {
		//3.创建Thread类的子类对象
		MyThread t1 = new MyThread();
		//4.通过此对象调用start():(1)启动当前线程(2)调用当前线程的run()
		t1.start();
		//继续创建新线程
		MyThread t1 = new MyThread();
		t2.start();
		//一下操作依然是在主线程中执行
		System.out.println(Thread.currentThread().getName());
	}
}

附:下面程序展示了创建Thread类的匿名子类的方式

new Thread(){
            @Override
            public void run() {
                //方法体
                    }
                }
            }
        }.start();

方式二:实现Runnable接口
/*创建步骤:
	1.创建一个实现了Runnable接口的类
	2.实现类去实现Runnable中的抽象方法run()
	3.创建实现类的对象
	4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
	5.通过Thread类的对象调用start()
*/
//1.创建一个实现了Runnable接口的类
class MThread implements Runnable{
	//2.实现类去实现Runnable中的抽象方法run()
	@Override
	public void run(){
		//方法体
	}
}

public class ThreadTest{
	public static void main(String[] args){
		//3.创建实现类的对象
		MThread mThread = new MThread();
		//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
		Thread t1 = new Thread(mThread);
		//5.通过此对象调用start()
		t1.start();
		//再启动一个线程
		Thread t1 = new Thread(mThread);
		t2.start();

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


方式三:实现Callable接口(JDK 5.0新增)
/*创建步骤:
	1.创建一个实现Callable接口的实现类
	2.实现call方法,将此线程需要执行的操作声明在call()中
	3.创建Callable接口实现类的对象
	4.将创建的实现类对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
	5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
	6.可以通过get()获取Callable中call()方法的返回值
*/
//1.创建一个实现Callable接口的实现类
class MyThread implements Callable{
	//2.实现call方法,将此线程需要执行的操作声明在call()中
	@Override
	public Object call() throws Exception{
		//方法体
		return object;//可以根据需要选择是否返回、返回什么
	}
}

public class ThreadTest{
	public static void main(String[] args){
		//3.创建Callable接口实现类的对象
		MyThread myThread = new MyThread();
		//4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask对象
		FutureTask futureTask = new FutureTask(myThread);
		//5.将FutureTask对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
		new Thread(futureTask).start();
		//6.可以通过get()获取Callable中call方法的返回值
		try{
			Object object = futureTask.get();
			//方法体
		} catch(InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
	}
}

如何理解实现Callable接口的方式比实现Runnable接口的方式更强大?
1.call()可以有返回值
2.call()可以抛出异常
3.Callable是支持泛型的


方式四:使用线程池(JDK 5.0新增)

经常创建和销毁使用量特别大的线程,对性能影响很大。可以提前创建好多个线程放入线程池中,使用时直接获取,用完放回池中,可以避免频繁销毁,实现重复利用

/*创建步骤:
	1.提供指定线程数量的线程池
	2.执行指定的线程的操作。需要提供显示Runnable接口或Callable接口实现类的对象
	3.关闭连接池
*/
class MyThread implements Runnable{
	@Override
	public void run(){
		//方法体
	}
}

class MyThread1 implements Runnable{
	@Override
	public void run(){
		//方法体
	}
}

public class ThreadPool{
	public static void main(String[] args){
		//1.提供指定线程数量的线程池
		ExecutorService service = Executors.newFixedThreadPool(10);
		ThreadPoolExecutor service1 = (ThreadPoolExecutor)service;
		//设置线程池的属性
		service1.setCorePoolSize(15);//设置核心池大小
		service1.setKeepAliveTime();//设置无任务时线程最多存活多长时间
		//2.执行指定的线程操作。需要提供实现Runnable接口或Callable接口实现类的对象
		service.execute(new MyThread());
		service.execute(new MyThread1());
		//service.submit(Callable callable);这是Callable的写法
		//3.关闭连接池
		service.shutdown();
	}
}

使用线程池的好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中的线程,不需要每次都创建)
3.便于线程管理


Thread中的常用方法
  • start():启动当前线程,调用当前线程的run()
  • currentThread():静态方法,返回执行当前代码的线程
  • getName():获取当前线程的名字
  • setName():设置当前线程的名字
  • yield():释放当前CPU的执行权,把执行权让给优先级相同或更高的线程。static方法,直接调用
  • join():在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态
  • sleep(long millitime):让当前线程阻塞指定的millitime毫秒,static方法,直接调用
  • isAlive():判断当前线程是否存活

线程的优先级
  • MAX_PRIORITY:10;
    MIN_PRIORITY:1;
    NORM_PRIORITY:5(默认优先级)
  • getPriority():获取线程的优先级
    setPriority(int p):设置线程的优先级
  • 高优先级抢占低优先级线程的CPU执行权,只是从概率上来说,高优先级的线程高概率被调度执行,并不意味着只有高优先级执行完后低优先级才能执行

线程的分类
  • 分为守护线程用户线程
  • 它们的区别是判断JVM何时离开(不懂,先挖个坑在这吧

线程同步机制

方式一:同步代码块
synchronized(同步监视器){
	//需要被同步的代码
}
  • 操作共享数据的代码,即为需要被同步的代码
  • 共享数据:多个线程共同操作的变量
  • 同步监视器,俗称“锁”。锁必须是唯一的,才能起到同步作用
  • 关于锁的选择:
    • 任何对象都可以作为同步锁,比如自己随便new一个对象当锁。因为所有对象都自动含有单一的锁
    • 一个线程类中,所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this)
    • 一定要确保同一共享数据的多个线程共用一把锁
方式二:同步方法
//把synchronized放在方法生命中,表示整个方法为同步方法
public synchronized void method(){
	//需要被同步的方法体
}

个人觉得,其实同步方法可以理解为,把同步代码块中的需要被同步的代码抽取出来,写成一个方法,加上一个synchronized声明,做成同步方法,扔给run()方法调用。本质上两种方法都一样的,都是“synchronized+锁”来保证同步。
同步方法仍然涉及到同步监视器,只是不需要我们显式的声明,自动选择了this当前类.class当同步监视器


方式三:Lock(JDK 5.0开始)
//以实现Runnable接口方式为例
//实例化ReentrantLock
private ReentrantLock lock = new ReenTrantLock();//这里的ReentrantLock()可以有参数,有的话就是boolean fair参数,表示是否遵守“先来先服务”
//如果是继承Thread类的创建方法,此处ReentrantLock必须是static的,否则每个对象都会有一个lock
public void method(){
	lock.lock();//上锁
	try{//lock一般都要放在try结构中,即使没有异常也要使用try-finally结构,因为要保证unlock一定会被执行
	//保证线程安全的代码;
	}
	finally{//需要把unlock写入finally中以保证锁一定会被释放
	lock.unlock();//解锁
	}
}

synchronized与Lock的对比
  • Lock是显式锁,手动开启/关闭;synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • Lock没有同步监视器,synchronized需要唯一的同步监视器
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • 优先使用顺序:Lock—>同步代码块—>同步方法

改写懒汉式单例模式
class SingleInstance{
	private SingleInstance(){}
	private static SingelInstance singelInstace = null;
	public static SingleInstance getInstance(){
		if(singelInstance == null){
			synchronized(SingleInstance.class){
				if(singelInstance == null){
					singleInstance = new SingleInstance();
				}
			}
		}
		return singleInstance;
	}
}

死锁

不同线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁


同步机制小结
  • 使用继承Thread类方法创建多线程时,由于会创建多个当前类对象,每一个线程都会有一个this,所以最好使用类.class当同步监视器
  • 使用实现Runnable接口方法创建多线程时,只创建了一个当前类对象,所以可以使用this充当同步监视器
  • 在选择需要同步的代码范围时,要合理进行选择。范围太小,不能起到同步作用;范围太大,限制了多线程的性能
  • 当前线程在同步时遇到breakreturn、各种异常、wait()等,都会暂停线程并释放锁。但是遇到sleep()yield()suspend()(挂起)时,不会释放锁
  • 同步的优缺点:
    • 优点:解决了线程的安全问题
    • 缺点:操作同步代码时,只能有一个线程参与,其它线程等待。相当于是一个单线程过程,效率较低

同步机制练习

程序实现:银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。

class Account{
    private double balance;

    public Account(double balance) {
        this.balance = balance;
    }

    //存钱
    public synchronized void deposit(double amt){
        if(amt > 0){
            balance += amt;

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + ":存钱成功。余额为:" + balance);
        }
    }
}

class Customer extends  Thread{

    private Account acct;

    public Customer(Account acct) {
        this.acct = acct;
    }

    @Override
    public void run() {

        for (int i = 0; i < 3; i++) {
            acct.deposit(1000);
        }

    }
}


public class AccountTest {

    public static void main(String[] args) {
        Account acct = new Account(0);
        Customer c1 = new Customer(acct);
        Customer c2 = new Customer(acct);

        c1.setName("甲");
        c2.setName("乙");

        c1.start();
        c2.start();
    }
}

线程通信

三个方法
  • wait():当前进程就进入阻塞状态,并释放同步监视器
  • notify():唤醒被wait()阻塞的一个进程。如果有多个线程被wait,则唤醒优先级最高的那个
  • notifyAll():唤醒所有被wait阻塞的线程
  • 一般wait()要和notify()notifyAll()搭配使用
  • 一个线程被wait后再被notify,会从上次wait的地方继续执行
  • 这三个方法必须使用在同步代码块或同步方法中,并且调用者必须是同步监视器。所以lock方法中无法使用
  • 这三个方法是定义在Object类中的

sleep()和wait()的异同:
相同点:都会使当前线程进入阻塞状态
不同点:1.声明位置:sleep声明在Thread类中,wait声明在Object类中
2.调用场景不同:sleep可以在任何需要的地方调用,wait只能在同步方法、同步代码块中被调用
3.释放同步监视器:如果都在同步代码块或同步方法中,sleep不会释放锁,wait会释放锁

代码示例:

/**
 * 线程通信的例子:使用两个线程打印 1-100。线程1, 线程2 交替打印
 */
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);//加sleep目的是,如果有安全问题,增大问题暴露概率
                    } 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();
    }
}

生产者消费者问题
/**
 * 线程通信的应用:经典例题:生产者/消费者问题
 *
 * 生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,
 * 店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员
 * 会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品
 * 了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
 *
 * 分析:
 * 1. 是否是多线程问题?是,生产者线程,消费者线程
 * 2. 是否有共享数据?是,店员(或产品)
 * 3. 如何解决线程的安全问题?同步机制,有三种方法
 * 4. 是否涉及线程的通信?是
 */
class Clerk{

    private int productCount = 0;
    //生产产品
    public synchronized void produceProduct() {

        if(productCount < 20){
            productCount++;
            System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个产品");

            notify();

        }else{
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
    //消费产品
    public synchronized void consumeProduct() {
        if(productCount > 0){
            System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个产品");
            productCount--;

            notify();
        }else{
            //等待
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

class Producer extends Thread{//生产者

    private Clerk clerk;

    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(getName() + ":开始生产产品.....");

        while(true){

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.produceProduct();
        }

    }
}

class Consumer extends Thread{//消费者
    private Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(getName() + ":开始消费产品.....");

        while(true){

            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.consumeProduct();
        }
    }
}

public class ProductTest {

    public static void main(String[] args) {
        Clerk clerk = new Clerk();

        Producer p1 = new Producer(clerk);
        p1.setName("生产者1");

        Consumer c1 = new Consumer(clerk);
        c1.setName("消费者1");
        Consumer c2 = new Consumer(clerk);
        c2.setName("消费者2");

        p1.start();
        c1.start();
        c2.start();

    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值