程序、进程、线程
程序:完成特定任务、用某种语言编写的一组指令的结合,一段静态的代码,静态对象;
进程:程序的一次执行过程,即正在执行的程序;
线程:进程可细化为线程,是一个程序内部的一条执行路径;
进程作为资源分配的单位,线程作为调度和执行的单位,有独立的运行站和程序计数器PC,线程开销小;
方法区和堆是一个进程一份,线程共享;
单核CPU和多核CPU:
并发:一个CPU在一段时间内执行多个任务;
并行:多个CPU同时执行多个任务;
多线程:
①提高应用程序响应,增强用户体验;
②提高CPU利用率
③改善程序结构,将长、复杂的进程分为多个线程;
java.lang.Thread
创建多线程
每个线程都是通过某个特定Thread对象的 run() 方法来完成操作的;
该Thread对象的start()方法来启动这个线程,而非直接调用run();
多线程的创建(方式一):
①创建一个继承于Thread类的子类
②重写Thread类的run()
③创建Thread类的子类的对象
④通过对象调用start() => 启动线程并执行run
Thread.currentThread().getName()
获取当前运行线程名字
Thread线程测试常用方法
创建线程的方式二:实现runnable接口
两种方式比较:
实现runnable接口从而创建线程比较好,像购票系统,只需要new一个实现了接口的对象,就可以将此参数传递到Thread类的构造器中从而创建多个Thread类的对象;
线程的几种状态:
why?
解决像买票这种线程安全问题;
线程同步、线程安全问题
两种同步机制:
①同步代码块 synchronized (锁){ 操作共享数据(多个线程共同操作的数据)的代码 }
任何一个类的对象(类也可以,真正的面向对象,反射会将)都可以充当锁,但多个线程必须共用同一个锁;
②同步方法:在方法修饰前加上synchronized,将同步代码放方法体内;同步方法仍然涉及到同步监视器,只是不需要显示声明,非静态同步方法,同步监视器是this,静态同步方法,同步监视器是当前类本身;
③lock锁;
实现runnable接口,同步监视器可以用this来充当;
如果用同步方法,且是继承Thread类的形式,那么同步类就要声明为
同步的优点及局限性:
使用同步机制将单例模式中的懒汉式改写为线程安全的:
class Bank { private Bank(){} private static Bank instance=null; /* 方法一 public static synchronized Bnak getInstance() { if(instance == null) { instance=new Bank(); } return instance; } */ // 方法2.1 public static Bnak getInstance() { // 效率稍差 synchronized(Bank.class) { if(instance == null) { instance=new Bank(); } return instance; } } // 方法2.2 public static Bnak getInstance() { if(instance == null) { synchronized(Bank.class) { if(instance == null) { instance=new Bank(); } } } return instance; } }
死锁问题
死锁:不同的线程相互占用同步资源,都在等待对方放弃自己需要的同步资源,形成死锁;
死锁不会有异常提示,所有线程处于阻塞状态,无法继续,要避免死锁;
lock锁
解决线程安全方式三:lock锁
private ReentrantLock lock = new ReentrantLock(fair=false)
若 fair = true,则先来先得,公平方式;若为false,则多人排队时下一个进入者随机;
private ReentrantLock lock = new ReentrantLock(fair=false); lock.lock(); 同步代码块 lock.unlock();
面试题:synchronized 与 lock 的异同
使用的优先顺序:lock -> synchronized同步代码块 -> 同步方法;
面试题:同步与异步
同步涉及到线程通信,多个线程访问临界区,只允许一个线程进去使用,其它的等待;
异步就是大家该走走,都并行地执行;
wait、notify
wait进入阻塞状态时会释放锁;
notifyAll 会唤醒所有 wait 的线程
注意:
面试题:sleep 和 wait 异同
练习:生产者消费者模型
import java.util.Arrays; import java.util.Scanner; public class test { public static void main(String[] args) { Goods goods = new Goods(); Producer producer = new Producer(goods); Comsumer comsumer = new Comsumer(goods); producer.setName("生产者1"); comsumer.setName("消费者1"); producer.start(); comsumer.start(); } } class Goods { private int num = 0; public synchronized void produceGoods() { if (num < 20) { num++; System.out.println(Thread.currentThread().getName() + ":开始生产第" + num + "个产品"); notify(); } else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void consumeGoods() { if (num > 0) { System.out.println(Thread.currentThread().getName() + ":开始消费第" + num + "个产品"); num--; notify(); } else { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Producer extends Thread { private Goods good = new Goods(); public Producer(Goods good) { this.good = good; } @Override public void run() { System.out.println(getName() + "开始生成产品..."); while (true) { try { sleep(10);// 如果要生产快些,那就让生产者睡得短些 } catch (InterruptedException e) { e.printStackTrace(); } good.produceGoods(); } } } class Comsumer extends Thread { private Goods good = new Goods(); public Comsumer(Goods good) { this.good = good; } @Override public void run() { System.out.println(getName() + "开始消费产品..."); while (true) { try { sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } good.consumeGoods(); } } }
JDK5.0 定增创建线程方式:Callable接口
步骤:
①创建一个实现Callable接口的类
②实现call方法,将此线程需要执行的操作声明在call()中;
③创建Callable接口实现类的对象
④将此对象作为参数传递到FutureTask构造器中,创建FutureTash对象;
⑤将FutureTash对象作为参数传递到 Thread 类的构造器,创建Thread类的对象并 start() ;
⑥获取Callable中call()的返回值;
比实现runnable接口创建线程要强大;
创建线程的方式四:线程池
开发中不会一个个去创建线程,都会有线程池;
比如手机看新闻下滑过程,网路不好时,你下滑可能出现了文字但没出现图像,这几乎是主线程加载文字,用多个分线程去加载图片,不影响用户观看;
开发有框架,不需要掌握具体实现,但要记住创建多线程的四种方式;
String
①JDK9之后是 byte[] 字节数组,主要是为了节省空间,还增加了一个coder字节,表示是否包含utf-16编码;
②String实现了Serializable接口:表示字符串支持序列化,序列化就是在这边是一个对象,通过网络传输字节过去后,在对面仍能够还原成这个对象;
③String内部定义了 final char[] value用于存储字符串数据;
④不可变性;
⑤当对字符串进行连接操作时,需要重写指定区域赋值,不能使用原有value进行赋值;
String str1=“abc” 和 String str2=new String(“abc”)的区别
注意含有字符串属性的类,字面对字符串的初始化也放常量池;
面试题:String str2 = new String(“abc”);在内存中创建了几个对象?
String str1 = "abc"; // 在常量池中 String str2 = new String("abc"); // 在堆上
当直接赋值时,字符串“abc”会被存储在常量池中,只有1份,此时的赋值操作等于是创建0个或1个对象。如果常量池中已经存在了“abc”,那么不会再创建对象,直接将引用赋值给str1;如果常量池中没有“abc”,那么创建一个对象,并将引用赋值给str1。
那么,通过new String(“abc”);的形式又是如何呢?答案是1个或2个。
当JVM遇到上述代码时,会先检索常量池中是否存在“abc”,如果不存在“abc”这个字符串,则会先在常量池中创建这个一个字符串。然后再执行new操作,会在堆内存中创建一个存储“abc”的String对象,对象的引用赋值给str2。此过程创建了2个对象。
当然,如果检索常量池时发现已经存在了对应的字符串,那么只会在堆内创建一个新的String对象,此过程只创建了1个对象。
图中两个String对象的value值的引用均为{char[3]@1355},也就是说,虽然是两个对象,但它们的value值均指向常量池中的同一个地址。当然,大家还可以拿一个复杂对象(Person)的字符串属性(name)相同时的debug结果进行比对,结果是一样的;
面试题:String str = “abc” + “def”;会创建几个对象
String str = "abc" + "def";
上面的问题涉及到字符串常量重载“+”的问题,当一个字符串由多个字符串常量拼接成一个字符串时,它自己也肯定是字符串常量。字符串常量的“+”号连接Java虚拟机会在程序编译期将其优化为连接后的值。
就上面的示例而言,在编译时已经被合并成“abcdef”字符串,因此,只会创建1个对象。并没有创建临时字符串对象abc和def,这样减轻了垃圾收集器的压力。
更深层的内容,参考文章:面试题系列第2篇:new String()创建几个对象?有你不知道的
字符串拼接
①常量与常量的拼接结果还在常量池,即常量池不会存在相同内容的常量;
②只要其中有一个是变量,结果就在堆中;
③如果拼接结果调用intern()方法,返回值就在常量池中;
④如果价格final 那么字符串就在常量池,那么拼接后还在常量池,比如final String s8="javaEE",String s9=s8+"hadoop"那么s3==s9 为true
面试题:字符串的不可变性
为什么ex.str
输出为good,因为String的不可变性,当String对象作为参数传递给函数时,的确传的地址,但函数内部试图修改该地址所指向的内容,由于String的不可变性,使得它会在常量区中再创建"test ok",使形参str等于它的地址,但并未改变实参str的地址;
还是记住那句话:基本数据类型,传递值;引用数据类型,传递地址
;
字符串常量区的演变
1、 java7之前,方法区位于永久代(PermGen),永久代和堆相互隔离,永久代的大小在启动JVM时可以设置一个固定值,不可变;
2、 java7中,static变量从永久代移到堆中;
3、 java8中,取消永久代,方法存放于元空间(Metaspace),元空间仍然与堆不相连,但与堆共享物理内存,逻辑上可以认为在堆中,但是实际上我们说的堆指的是用于存放java对象的那些空间;
String 与 基本数据类型、包装类的转换:
String 与 char[] 数组的转换
String -> char[] String.toCharArray()
char[] -> String 构造器;
String 与 byte[] 之间的转换:调用String的getBytes(可指定编码类型);
StringBuffer