一、程序、进程、线程
什么是程序(Program)?
我见过最多的说法是——程序=数据结构+算法。
程序代指为了完成特定的任务、采用某种语言编写的一组指令的集合,即一段静态的代码。
进程(process)
程序的一次执行过程,或正在运行的一个程序。它是一个动态的过程,有生命周期。进程是资源分配的单位,系统在运行时会为不同的进程分配不同的内存区域。
线程(Thread)
可以理解为一个进程的各个功能部分,一个程序的内部的一个执行路径,
多线程:一个进程同时并行执行多个线程
线程是调度和执行的单位,每个线程有独立的栈和程序计数器pc,线程切换的开销较小
一个进程中的多个线程共享相同的内存单元/地址空间,可访问共有资源,这使得线程间的通信变得灵活高效,但也存在安全隐患。
一个java应用程序至少有三个线程:main(),gc(),异常处理线程。
**并行:**多个CPU同时执行多个任务
**并发:**一个CPU同时执行多个任务(时间片轮转)
多线程优点
单核CPU:单线程的完成多个任务要比多线程完成多个任务要快,因为省去了CPU切换的时间
但多线程能: 提高应用程序的响应、提高计算机CPU的利用率、改善程序结构,
何时需要多线程:
- 需要同时执行多个任务
- 需要实现一些需要等待的任务
- 需要一些后台运行的程序
创建多线程的第一种方式
- 定义一个类继承Thread
- 重写run方法
- 实例化该类
- 调用start方法开启线程
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("新线程id:"+Thread.currentThread().getId());
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setName("线程1:");// 给线程命名
Thread.currentThread().setName("主线程:");
myThread.start(); //开启新线程,会调用run方法
System.out.println("main线程id:"+Thread.currentThread().getId());
}
}
已经start()的线程再次start()会抛illegalThreadStatus异常
该方式存在线程安全问题:多个线程共用一个数据时存在问题
其它方法:
- yield():释放当前CPU的执行权
- join():在A线程中调用B线程的join方法,A会阻塞,直到B执行完成,A结束阻塞状态,在分配到执行权后继续执行
- sleep(long milseconds):线程睡眠
- isAlive():判断当前线程是否存活
线程调度
调度策略:时间片、抢占式(高优先级的线程先执行)
Java的调度方式:同优先级的线程先进先出队列,使用时间片策略;对高优先级,使用抢占式策略
线程优先级等级-常用
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5 默认
getPriority(): 得到优先级的值
setPriority(int value): 设置优先级
线程创建时会继承父类的优先级等级,低优先级只是获得调度的概率低,并非是在高优先级调度之后才执行
创建多线程的第二种方法
- 创建一个类实现Runnable接口
- 重写run方法
- 创建实现类的对象
- 将该对象作为参数传递到Thread的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
注意点:start调用的是当前线程的run,
@Override
public void run() {
if (target != null) {
target.run();
}
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
所以最终调用的还是自己的run方法
该方式能够共享数据,且是实现的方式,可以继承其它的类,所以优先选择这种方式
Thread也实现了Runnable接口
线程的生命周期
JDK中用Thread.State类定义了线程的几种状态
- 新建:新生的线程处于新建状态
- 就绪:处于新建状态的线程被start()后,进入线程队列等待CPU时间片,此时已具备了运行的条件,只是没分配到CPU资源
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run定义了线程的操作和功能
- 阻塞:在一些特殊情况下,被挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致
线程安全问题
当一个线程在操作公有数据时,还未完成操作,其它线程参与进来,也操作该数据,就会出现问题。
解决方案:当一个线程在执行操作时,禁止其它线程参与进来,直到该线程操作完成,其他线程才可以进行操作。即使线程出现阻塞,其它线程也不能进行操作。 (同步代码块、同步方法、Lock锁)
Java中通过同步机制,解决线程安全问题
方式一:同步代码块
synchronized(同步监视器){//俗称锁,任何类的对象,都可充当锁
需要被同步的代码,即操作公有数据的代码
}
这里要求多个线程共用一把锁
在使用继承的方式时要注意把共享数据和锁定义成静态的,保证只有一份,可考虑使用当前类作为锁(类也是一个对象)
在使用实现的方式时可用this代替锁
方法二:同步方法
如果操作共享数据的代码完整地声明在了一个方法中,我们可以将此方法声明为同步的,写法如下。默认同步监视器就是this
// 以实现接口式为例
@Override
public void run() {
while (true){
show();
}
}
public synchronized void show(){
if (tickts>0){
System.out.println(getName()+"窗口买票"+tickts+"号.");
tickts--;
}
}
// 以继承式为例
@Override
public void run() {
while(true){
show();
}
}
// 这里的锁就是当前类对象
private static synchronized void show(){
if (tickets>0){
System.out.println(Thread.currentThread().getName()+"窗口买票:"+tickets+"号。");
tickets--;
}
}
同步的方式
能解决线程安全问题,但操作同步代码时,只能有一个线程参与,其它线程等待,相当于是一个单线程的过程,效率低
死锁
不同的线程分别占用对方的同步资源不释放,都在等待对方释放自己所需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,线程处于阻塞状态,无法继续
解决办法:尽量减少同步资源的定义,尽量避免嵌套同步,专门算法
Lock锁
JDK5.0以后,可以显示定义同步锁对象来实现同步。同步锁使用Lock对象充当。
public class MyLock implements Runnable{
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try {
lock.lock(); // 上锁
if (ticket>0){
System.out.println(Thread.currentThread().getName()+":"+ticket);
ticket--;
}else
break;
} finally {
lock.unlock();// 解锁
}
}
}
public static void main(String[] args) {
MyLock myLock = new MyLock();
Thread t1 = new Thread(myLock);
Thread t2 = new Thread(myLock);
Thread t3 = new Thread(myLock);
t1.start();
t2.start();
t3.start();
}
}
synchronized和lock的区别
synchronized在执行完代码后会自动释放同步监视器
lock需要手动的启动和释放,显示锁,且只有代码块锁和方法锁;JVM花费较少的时间来调度线程,性能好,更好的扩展性
一个例子
class Account{
private int money;
public Account(int money) {
this.money = money;
}
public synchronized void saveMoney(int money2){
money+=money2;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"存了"+money2+",账户余额为:"+money);
}
}
public class Example extends Thread{
private Account account;
public Example(Account account) {
this.account=account;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
account.saveMoney(1000);
}
}
public static void main(String[] args) {
Account acc = new Account(0);
Example c1 = new Example(acc);
Example c2 = new Example(acc);
c1.start();
c2.start();
}
}
分析:这里我们重写了继承Thread类的构造方法,获得了共享数据和方法,start()方法调用的还是当前线程的run方法,因为没有传入Runnable实现类的对象,"target"为空。这里的锁是Account的对象,只有一个。run方法没有抛异常,我们只能try-catch。
线程的阻塞与唤醒 进程间通信
wait():线程执行该方法后进入阻塞状态,同时释放锁
notify():线程执行该方法后会唤醒一个线程
notifyAll():唤醒所有的线程
这三个方法必须是同步代码块或同步方法中的同步监视器,因此这三个方法必须使用在同步方法或同步代码块中。这三个方法是被定义在java.lang.Object类中的。
class NUmber implements Runnable{
private int i = 0;
@Override
public void run() {
while(true){
synchronized (this) {
notify();
if (i<=100){
System.out.println(Thread.currentThread().getName()+"打印了:"+i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
}else
break;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class CommunctionTest {
public static void main(String[] args) {
NUmber num = new NUmber();
Thread t1 = new Thread(num);
Thread t2 = new Thread(num);
t1.start();
t2.start();
}
}
线程1执行wait后释放锁,进入阻塞;之后线程2才可以拿到锁,先执行notify()唤醒了线程1,此时锁在线程2上,等线程2执行了wait后,释放锁,线程1才能拿到锁。
sleep()和wait()
相同点:都可以使当前线程进入阻塞状态
不同点:声明位置不同,sleep在Thread类中,wait在Object类中
sleep可在任意需要的情况下调用;wait必须在同步代码块或同步方 法中,
wait会释放同步监视器,sleep不会(前提是在同步方法或代码块 中)
案例:生产者与消费者
package exercise;
class Clerk{
private int products = 0;
public synchronized void createProduct() {
if (products<20){
products+=5;
System.out.println(Thread.currentThread().getName()+"正在生产第"+products+"个产品");
notify();
}else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void customProduct() {
if (products>0){
System.out.println(Thread.currentThread().getName()+"正在消费第"+products+"个产品");
products--;
notify();
}else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Productor extends Thread{
private Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始生产......");
while(true){
clerk.createProduct();
try {
sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Custom extends Thread{
private Clerk clerk;
public Custom(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"开始消费......");
while(true){
clerk.customProduct();
try {
sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class CustomAndProductor {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor productor = new Productor(clerk);
Custom custom = new Custom(clerk);
productor.setName("生产者");
custom.setName("消费者");
productor.start();
custom.start();
}
}
分析:在Productor和Custom中重载构造函数是为了二者能够共用一份数据。将生产和消费方法同步,即可解决线程安全问题。生产之后便可唤醒消费者,消费之后便可唤醒生产者。
创建多线程的新方式
方式一:实现Callable接口
// 1. 创建一个类,实现Callable接口
public class MyCallable implements Callable {
// 2. 重写call方法
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
if (i % 2 ==1){
sum += i;
}
}
return sum; //这里会自动装箱
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 3. 创建实现类对象
MyCallable myCallable = new MyCallable();
// 4.将该对象作为参数传递到FutureTask的构造器中,创建FutureTask对象
FutureTask futureTask = new FutureTask(myCallable);
// 5. 将FutureTask对象做为参数传递到Thread构造器中,创建对象,并开启线程
Thread thread = new Thread(futureTask);
thread.start();
// 可获得线程的返回值
Object o = futureTask.get(); // 此方法可获得call()的返回值
System.out.println("和为:"+o );
}
}
该方式比实现Runnable接口的方式强大:
- call()方法有返回值
- call()方法可抛出异常,被外面的操作捕获,获取异常信息
- Callable支持泛型
方式二:使用线程池
思路:提前创建好多个线程,放入池中,使用时直接获取,用完放回池中,可避免重复创建、销毁,重复利用。这种方式可提高响应速度,降低资源消耗,便于线程管理
线程池相关API ExecutorService和Executor
ExecutorService:真正的线程池接口,常见子类有ThreadpoolExecutor,
方法:
执行指令,无返回值,一般用来执行Runnable | void executor(Runnable command) |
---|---|
执行任务,有返回值,一般执行Callable | Future submit(Callable task) |
关闭 | shutdown() |
Executor工具类
创建一个可根据需要创建新线程的线程池 | newCacheThreadPool() |
---|---|
创建一个可重用固定线程数的线程池 | newFixedThreadPool(n) |
只有一个线程的线程池 | newSingleThreadPool() |
可安排在在给定延迟后执行命令或定期执行的线程池 | newScheduledThreadPool(n) |
//创建线程
ExecutorService service = Executors.newFixedThreadPool(5);
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor)service;
threadPoolExecutor.setCorePoolSize(10);
//设置相关属性
((ThreadPoolExecutor) service).setCorePoolSize(20);
// 执行线程
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("使用线程池创建了线程。");
}
});
service.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
return null;
}
});
service.shutdown();
}
创建线程的四种方法
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 使用线程池