JavaSE-多线程基础-基于多线程的计时器应用

目录

一、进程与线程的含义与区别

1、含义:

2、区别:

二、线程的四种创建方式及使用

1、继承Thread类实现线程的创建

2、使用Runnable接口实现线程的创建

3、使用Callable接口实现线程的创建

4、线程池的方式

5、线程的常用方法:

三、线程的生命周期

 四、多线程的安全

1、同步代码块以及同步方法

1)同步代码块

2)同步方法

3)一些操作是否会释放锁

4)关于死锁

2、lock锁保护线程安全

3、synchronized 与 Lock 的对比

五、多线程的通信

六、基于多线程的计时器


一、进程与线程的含义与区别

1、含义:

进程(process):进程是程序的一次执行过程,或者是正在运行的一个程序。是一个动态
的过程:有它自身的产生、存在和消亡的过程——生命周期

线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径,如果一个进程可以同时执行多个线程,那么他就是支持多线程的。

        线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程
元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任
务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。

2、区别:

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单

资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大
的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己
独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共
同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是
相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整
个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口. 顺序执行序列和程序出口。但是线程不能独立
执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

二、线程的四种创建方式及使用

1、继承Thread类实现线程的创建

对于继承Thread的方式创建一个线程时我们需要重写Thread类里的run方法,设置为我们想要线程执行的任务。如下所示:

public class MyThreadClass extends Thread {

    public String name;

    public MyThreadClass(String name) {
        this.name = name;
    }


    @Override
    public void run() {
        for (int index = 0; index < 100; index++) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(index == 60) {
                yield();
            }
            System.out.println(Thread.currentThread().getName() + "index :" + index + " " + name);
        }
    }
}

MyThreadClass即是继承了Thread类的子类,这样一个线程就被创建了。

如果要使用这个线程只需要new一个子类的对象,此时一个线程对象被新建,然后使用对象调用Thread的start方法使得线程进入就绪态。等待JVM调度资源执行run方法。

    MyThreadClass t1 = new MyThreadClass("老大");
    t1.start();

        关于使用继承Thread的方式来创建一个线程的好处是在后续进程的开启比较方便,使用继承了Thread的子类的对象即可,而且如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this即可。坏处是使用继承的局限性,一个子类只能有一个父类,无法再继承其他类。

2、使用Runnable接口实现线程的创建

第二种方式是使用Runnable接口来创建一个线程,使用Runnable接口新建一个类时会强制我们实现run方法,以便线程的运行。创建过程如下:

public class Bank implements Runnable{
    private static int money;
    public static Object obj = new Object();

    public Bank() {

    }


    @Override
    public void run() {
        for (int index = 0; index < 3; index++) {
            synchronized(obj) {
                money += 1000;
                System.out.println(Thread.currentThread().getName() + "存完钱后,账户余额:" + money);

                obj.notify();

                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

这个Bank类就是基于实现Runnable接口的一个线程,关于里面出现的synchronized是为了保护线程的安全,在下面会说到。

对于这类线程我们不能简单的使用Bank类的对象来开启一个线程,我们需要基于这个类new 一个Thread的对象,即new Thread(bank类的对象); 如下所示:

public static void main(String[] args) {
        Bank bank = new Bank();

        Thread t1 = new Thread(bank);
        Thread t2 = new Thread(bank);

        t1.setName("甲");
        t2.setName("乙");

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

这样创建线程的好处是只是实现了Runnable的接口,还可以继承其他类,使得这个类可以更加丰富,但是坏处是这样创建线程比较复杂较于继承的方式,而且如果想要得到当前的线程只能使用Thread.currentThread()方法。

3、使用Callable接口实现线程的创建

在JDK1.5之后出现了使用Callable接口和线程池的方式来创建线程,第三种方式是使用Callable接口实现线程的创建,这种方式和使用Runnable接口的方式比较相似,不过这种方式的call方法(类似于run方法)存在一个返回值,我们可以使用get方法得到这个值以便我们需要得到一个线程的结果来进行下一步测试,实现方式如下:

public class ThreadCallable implements Callable {
    public int num;

    public ThreadCallable() {
    }

    @Override
    public Object call() throws Exception {
        for (int index = 0; index < 100; index++) {
            num += index;
            System.out.println(Thread.currentThread().getName() + index);
        }
        return num;//int 类型被自动封装为Integer类型
    }
}

而对于实现Callable接口的类,它的进程的开启只能通过先实例化一个FutureTask对象,然后基于此对象再实例化一个Thread对象才能调用call方法开启线程。如下所示:

public static void main(String[] args) {
        ThreadCallable t = new ThreadCallable();
        FutureTask task = new FutureTask(t);
        Thread thread = new Thread(task);

        thread.setName("数字线程:");
        thread.start();

        try {
            Object res = task.get();
            System.out.println("总和为:" + res);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

对于这种方式,它的功能较Runnable更为强大,它的call方法可以拥有返回值,且返回值支持泛型,也可以抛出异常。但是它的缺点也显而易见,代码较为复杂,开启线程比较复杂。

4、线程池的方式

第四种创建线程的方式是线程池的方式,一次创建一定数量的线程,使用完毕后会闲置等待下一次的开启,不会频繁的创建和销毁线程。如下所示:

public class TestThreadPool {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor servicePool = (ThreadPoolExecutor)service;

        //设置核心池的大小(由于ExecutorService是一个接口,
        // 无法直接调用方法,而ThreadPoolExecutor是接口的实现类,故可以直接调用 )
        //((ThreadPoolExecutor) service).setCorePoolSize();
        servicePool.setCorePoolSize(10);

        servicePool.setMaximumPoolSize(50);//设置最大线程数
//        servicePool.setKeepAliveTime();//线程没有任务后多久终止

        service.execute(new Bank());//适合Runnable接口的实现类
        //service.submit();适合使用Callable接口的实现类
        service.shutdown();
    }
}

线程池的类是ThreadPoolExecutor类,但是我们可以使用ExecutorService接口创造一个线程池,关于线程池我们可以使用execute方法来启动一个实现了Runnable接口的线程,而submit可以启动一个实现了Callable接口的线程。

        我们可以使用一些方法来管理线程池,比如setMaximumPoolSize(50);来设置最大线程数,setCorePoolSize来设置核心线程数,以及使用setKeepAliveTime来设置线程没有任务后多久结束。

        线程池的优点是减少了线程创建的时间,提高了效率,降低了资源消耗,便于管理线程。但是线程池需要我们手动关闭,使用shutdow()方法来关闭线程池。

5、线程的常用方法:

Thread.currentThread()方法是得到当前线程

start()方法是开启一个线程,使得JVM可以调用run方法

getName()得到线程的名字,必须是由线程来调用,如果是基于继承的话可以使用this得到线程,其他则需要使用Thread.currentThread()方法得到当前线程。

join()方法:当前执行的线程阻塞,然后执行此线程。

setName()方法是设置线程的名字,一般通过Thread类的对象来调用。

三、线程的生命周期

JDK中用Thread.State类定义了线程的几种状态。要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建
状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已
具备了运行的条件,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线
程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中
止自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

 四、多线程的安全

        对于多线程最重要的一点就是安全性,不能发生不安全的现象,例如下面这个简单的卖票线程,同时由三个窗口售卖100张票:

public class Ticket implements Runnable {
    private static volatile int num = 100;
    private Object lock;

    public Ticket() {
        this.lock = new Object();
    }

    @Override
    public void run() {

            while (true) {
                
                   if (num > 0) {
                       try {
                           Thread.sleep(100);
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
                       System.out.println(Thread.currentThread().getName() + "售出:" + num);
                        num--;
                    } else {
                        break;
                    }
            }
    }
}

开启三个卖票线程后结果如下:

可以明显的看到票的数量存在重复以及错票等问题,出现这个问题的原因在于我们开启的三个线程存在着共享数据,即票的数量,而三个线程的优先级相同,无法判断执行顺序,而且很有可能发生多个线程同时操作数据,导致数据错乱的情况,那么我们需要保护线程的安全就要做到当有一个线程在操作共享数据时,其余线程无法操作,只有等前一个线程操作完成了其他线程才可继续执行。Java对于多线程的安全问题提供了专业的解决方式:同步机制

1、同步代码块以及同步方法

1)同步代码块

        关于同步代码块的含义是使用synchronized关键字构造一个代码块,并且把一个对象作为锁来锁住这个代码块来保证同时只会有一个线程在操作共享数据,以此来保证线程的安全性。如下所示:

                synchronized (lock) {
                    if (num > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "售出:" + num);
                        num--;
                    } else {
                        break;
                    }
                }

结果如下:

可以看到确实保护了多线程的安全。

对于同步代码块有以下需要注意的地方:

1、所有的需要操作共享数据的线程的锁只能是一种,而且锁必须是一个对象,但是我们也可以使用线程的创建类的.class作为锁,因为面向对象编程的思想,.class其实也是一个对象。

2、同步代码块所包含的地方一定是对共享数据有操作的地方,既不可包含过多也不可过少,包含过多会导致一个线程把所有的任务完成,变为单线程,而包含过少的话会使得多线程的安全问题没有完全解决。

2)同步方法

synchronized还可以放在方法声明中,表示整个方法为同步方法例如:
public synchronized void show (String name){
….
}

对于同步方法其实就是把同步代码块里的内容创建一个新的方法,并且加入synchronized关键字,在run方法里调用同步方法即可达到包含多线程安全的问题。

 public synchronized void fun() {
        if (num > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "售出:" + num);
            num--;
        } else {
            return;
        }
    }

切记:同步方法里包含的与同步块中所包含的要求相同,需要仔细考虑!

3)一些操作是否会释放锁

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

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

4)关于死锁

        对于死锁问题的出现一般是由于在一个同步代码块中嵌套了同步代码块,导致在两个线程进入不同的代码块中,且两者均锁住了对方的代码块,导致二者均无法继续运行,造成死锁的问题。

        不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。

        对于死锁的解决办法就是我们在编写多线程代码时要注意避免同步代码块的嵌套,尽力减少同步资源的定义,从根源解决问题。

2、lock锁保护线程安全

        从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象
加锁,线程开始访问共享资源之前应先获得Lock对象。ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。如下所示:

class A{
        private final ReentrantLock lock = new ReenTrantLock();
        public void m(){
                       lock.lock();
                       try{
                                //保证线程安全的代码;
                        }
                        finally{
                                lock.unlock();
                        }
         }
}

注意:如果同步代码有异常,要将unlock()写入finally语句块!

3、synchronized 与 Lock 的对比

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

五、多线程的通信

        线程的通信的意思就是几个线程交替进行,使同等优先级的线程的无序性得到改变,解决生产者消费者问题。这个问题的解决需要我们用到Thread的两个方法wait方法和notify方法。

关于wait() 与 notify() 和 notifyAll()
 wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当
前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有
权后才能继续执行。
notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
notifyAll ():唤醒正在排队等待资源的所有线程结束等待.

        这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报
java.lang.IllegalMonitorStateException异常。       

        因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。

下面这个是甲乙轮流去银行存钱然后显示存完钱之后的余额:

public void run() {
        for (int index = 0; index < 3; index++) {
            synchronized(obj) {
                money += 1000;
                System.out.println(Thread.currentThread().getName() + "存完钱后,账户余额:" + money);

                obj.notify();

                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }

结果如下:

六、基于多线程的计时器

通过多线程的通信原理,编写出了基于多线程的计时器,每一秒输出一次时间戳,误差在10毫秒以内。

public class Didadida implements Runnable {
	private static final long DEFAULT_DELAY_TIME = 1000;
	private volatile boolean goon;
	private long delayTime;
	private Runnable task;
	private Object lock;
	
	public Didadida() {
		this.goon = false;
		this.delayTime = DEFAULT_DELAY_TIME;
		this.lock = new Object();
	}

	public Didadida(long delayTime, Runnable task) {
		this();
		this.delayTime = delayTime;
		this.task = task;
	}
	
	public Didadida(long delayTime) {
		this(delayTime,null);
	}

	public void setDelayTime(long delayTime) {
		this.delayTime = delayTime;
	}

	public void setTask(Runnable task) {
		this.task = task;
	}
	
	public void start() throws Exception {
		if(this.task == null) {
			throw new Exception("用户未指定计时器任务!");
		}
		
		if(this.goon == true) {
			return;
		}
		
		this.goon = true;
		synchronized (lock) {
			new InnerRunnable();
			lock.wait();
		}
		
		new Thread(this,"计时器").start();
	}
	
	@Override
	public void run() {
		while(this.goon = true) {
			try {
				Thread.sleep(this.delayTime);
				synchronized(lock) {
					lock.notify();
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	class InnerRunnable implements Runnable {
		
		public InnerRunnable() {
			new Thread(this).start();
		}

		@Override
		public void run() {
			synchronized(lock) {
				lock.notify();
				try {
					lock.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			
			while(goon = true) {
				synchronized (lock) {
					try {
						task.run();
						lock.wait();
		
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		}
	}
}

测试结果如下所示:

 

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值