程序、进程、线程的理解
1.程序(program)
概念:是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
2.进程(process)
概念:程序的一次执行过程,或是正在运行的一个程序
说明:进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域
3.线程(thread)
概念:进程可进一步细化为线程,是一个程序内部的一条执行路径。
说明:线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小
每个线程,拥有自己独立的:栈、程序计数器(pc)
多个线程,共享同一个进程中的结构:方法区、堆
单核cpu和多核cpu的理解
单核其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务。
如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
一个Java应用程序java.exe,其实至少有三个线程:main()主线程,gc() 垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程。
并行与并发
并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事。
创建多线程的四种方式
1.继承Thread类的方式
start()作用:
1.启动当前线程
2.运行当前线程run()
注意:
3.**不能直接调用run()**来启动线程,这样只是单线程,不是多线程
4.再启动一个线程时,不能让已经start()的线程再执行,会报illegalThread异常,需要重新创建一个线程子类的对象
public class ThreadTest {
public static void main(String[] args) {
//3.创建Thread类的子类的对象
MyThread thread = new MyThread();
//4.通过此对象调用start()
thread.start();//线程1
// thread.run();
// thread.start();
MyThread thread1 = new MyThread();
thread1.start();//线程3
for (int i=0;i<100;i++){//线程2
if(i%2==0){
System.out.println(i+"***main***");
}
}
new Thread(){//还可以用匿名子类匿名对象的方法来使用多线程
@Override
public void run() {
super.run();
}
}.start();
}
}
//1.创建一个继承于Thread类的子类
class MyThread extends Thread {
//2.重写Thread类的run()-->将此线程执行的操作声明在run()中
public void run(){
for(int i=0;i<100;i++){
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
2.实现Runnable接口
//1.创建一个实现了 Runnable接口的类
class MyThread1 implements Runnable{
@Override
//2.实现类去实现Runnable中的抽象方法:run()
public void run() {
for(int i=0;i<100;i++){
if(i%2 ==0){
System.out.println(i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
// 3.创建实现类的对象
MyThread1 myThread1 = new MyThread1();
// 4.将此对象作为参数传递到 Thread 类的构造器中,创建Thread类的对象
Thread thread = new Thread(myThread1);
thread.setName("线程二");
// 5.通过Thread类对象来调用start()
// 5.1启动线程
// 5.2调用当前线程的run(),但由于是实现并不是继承,不是方式一那种调用重写的run()
// 通过查看源码可以知道 Thread的构造器形参是 Runnable类型的,
// 而步骤三创建的实现类的对象作为实参放进Thread构造器里,
// 当调用start()时会启动Thread类中的run方法,转而调用到形参的run(),也就是实现类的对象的run()
thread.start();
}
}
两种方式的对比
开发中:优先选择实现Runnable接口的方式
原因:
1.该方式没有类的单继承性的局限性
2.该方式更适合来处理多个线程有共享数据的情况(第一种方式想要共享数据需要声明static)
联系:public class Thread implements Runnable(第一种方式也同样实现了Runnable)
相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中
3.实现callable接口
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
- call()可以有返回值的。
- call()可以抛出异常,被外面的操作捕获,获取异常的信息
- Callable是支持泛型的
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.创建Callable接口实现类的对象
NumThread numThread = new NumThread();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
try {
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
4.线程池
好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;//向下转型
//设置线程池的属性
// System.out.println(service.getClass());
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
// service.submit(Callable callable);//适合使用于Callable
//3.关闭连接池
service.shutdown();
}
}
线程中的常用方法
* 1.start():启动当前线程,调用当前线程的run()
* 2.run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
* 3.currentThread():静态方法,返回执行当前代码的线程
* 4.getName():获取当前线程的名字
* 5.setName():设置当前线程的名字
*
* 6.yield():释放当前cpu的执行权,即使释放了,也不一定就执行其他线程
* 7.join():在线程a中调用线程b的join(),则a会阻塞,直到b执行完
* 8.stop():已过时,当执行此方法时,强制结束当前线程。
* 9.sleep(long millitime):让当前线程睡眠指定的millitime毫秒,即这段时间阻塞
* 10.xxx.isAlive():判断当前线程是否存活
线程的优先级
* 1. MAX_PRIORITY:10
* 2. MIN_PRIORITY:1
* 3. NORM_PRIORITY:5 -->默认优先级
* 4.如何获取和设置优先级
* getPriority():获取线程的优先级
* setPriority(int p):设置线程的优先级
* 说明:设置了高优先级的线程不一定比低优先级的线程先执行完,只是从概率上来说高的优先于低的执行
线程的生命周期
-
状态:五种
方法:对应状态转换使用的方法
-
关注:状态的改变导致哪些方法的执行;主动调用某些方法引起状态的改变
-
线程的最终状态都是死亡
线程的同步机制
背景
例子:创建三个窗口卖票,总票数为100张,使用实现Runnable接口的方式
- 1.问题:卖票过程中,出现了重票、错票 -->出现了线程的安全问题
- 2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
- 3.如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
解决方法(利用同步机制)
方式一:同步代码块
synchronized(同步监视器){
//需要被同步的代码
}
说明:
1.操作共享数据的代码,即为需要被同步的代码。
–>不能包含代码多了,也不能包含代码少了(关键!)。
2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
3.同步监视器,俗称:锁。 任何一个类的对象 ,都可以充当锁。
要求:多个线程必须要共用 同一把锁 。
4.在实现Runnable接口创建多线程的方式中,我们可以考虑使用 this 充当同步监视器。(当前方法对应类对应的对象)
5.在继承Thread类创建多线程的方式中,慎用 this 充当同步监视器(因为继承的这种方式需要创建多个类的对象,而this无法指代是哪个对象),考虑使用当前类充当同步监视器。
方式二:同步方法
- 同步方法仍然涉及到同步监视器,只是不需要显式声明。
- 非静态的同步方法,同步监视器是:this
静态的同步方法,同步监视器是:当前类本身
class Windows3 implements Runnable{
private int ticket=100;
@Override
public void run() {
while(true){//这个结构是无限循环,需要break来跳出循环
show();
if(ticket==0){
break;
}
}
}
//同步方法,需要用synchronized修饰,同一时刻只允许一个线程进入处理,结束出来下一个线程才能进去
public synchronized void show(){//同步监视器:this
if(ticket >0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
ticket--;
}
}
}
public class WindowTest3 {
public static void main(String[] args) {
Windows3 w = new Windows3();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
方式三:lock锁
class Window implements Runnable{ private int ticket = 100; //1.实例化ReentrantLock private ReentrantLock lock = new ReentrantLock();//关键 @Override public void run() { while(true){ try{ lock.lock();//2.调用锁定方法lock() if(ticket > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket); ticket--; }else{ break; } }finally { lock.unlock();//3.调用解锁方法:unlock() } } }}public class LockTest { public static void main(String[] args) { Window w = new Window(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); }}
-
面试题:synchronized 与 Lock的异同?
相同:二者都可以解决线程安全问题 不同:
synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器(自动的)
Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())(手动的,更灵活) -
优先使用顺序:
Lock -->同步代码块(已经进入了方法体,分配了相应资源) --> 同步方法(在方法体之外)
面试题:如何解决线程安全问题?有几种方式,同步机制解决,四种
利弊
同步的方式,解决了线程的安全问题。—好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。 —局限性
单例模式懒汉式的线程安全
public class BankTest { private BankTest(){ } private static BankTest instance = null; public static BankTest getInstance(){ if(instance == null){//双重检查,第一层检查。若已经创建了对象,后面有多个线程再进来时便不需要再进入,直接return synchronized (BankTest.class){//第二层检查。第一次多个线程进来时对象还未创建,因此需要同步来开启线程安全 if (instance ==null){ instance = new BankTest(); } } }return instance; }}
死锁
- 死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 说明:
1)出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
2)我们使用同步时,要避免出现死锁。
public class ThreadTest { public static void main(String[] args) { StringBuffer s1 = new StringBuffer(); StringBuffer s2 = new StringBuffer(); //线程一 new Thread(){ @Override public void run() { synchronized (s1){//起初先握住s1z s1.append("a"); s2.append("1"); try { Thread.sleep(100); //在睡眠的这段时间里,移交运行权给到另一个线程,此时另一个线程在握住s2这把锁之后也睡眠了,运行权再次回到原线程,但下一个同步问题需要s2这把锁,但却因为另一个线程握住s2而并未松开导致出现死锁 } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s2){ s1.append("b"); s2.append("2"); System.out.println(s1); System.out.println(s2); } } } }.start(); //线程二 new Thread(new Runnable() { @Override public void run() { synchronized (s2){ s1.append("c"); s2.append("3"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s1){ s1.append("d"); s2.append("4"); System.out.println(s1); System.out.println(s2); } } } }).start(); }}
线程通信
涉及到的三个方法
wait()
一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。(关键在于释放同步监视器)
notify()
一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
notifyAll()
一旦执行此方法,就会唤醒所有被wait的线程。
说明
- 三个方法只能在 同步代码块或同步方法 中使用。(不能在lock()中使用)
- 三个方法的调用者必须是同步代码块或同步方法中的同步监视器。(是同步监视器调用的),否则,会出现IllegalMonitorStateException异常
- 三个方法是定义在java.lang.Object类中。(因为同步监视器可以是任何类的对象,因此这三个方法必须定义在object类中,才能让任何类的对象随便调用)
面试题:sleep() 和 wait()的异同?
1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
2.不同点:
1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁(同步监视器),wait()会释放锁。