并行和并发
并发和并行非常相似。但是并发讨论得更多
-
并行:A和B在同一时间点一起运行
-
并发:A和B在同一时间段先后运行。但是这个所谓的同一时间段非常非常短。在我们的眼中就好像是同一时间点发生的。当然这是得益于cpu的高速。
进程与线程
进程Process
进程是一个在内存中运行的一个程序,拥有自己的一块内存空间。一个进程可以包含多个线程,至少一个线程。
线程Thread
线程是一种概念,一个进程中至少包含1个线程,最多没有限制,大概吧。堆空间共享,栈空间私有。在java里每个线程都是对象,这也是为什么会扯到堆栈空间的原因。
java程序的在系统中运行至少包含一个主线程(main线程)和一个GC线程。
线程调度
计算机只有一个CPU时,只能执行一条计算机指令。而显而易见的是,我们的电脑能听音乐的同时也能打游戏还能聊QQ。 上面这些作为线程进行理解,不论是听音乐,还是打游戏,聊QQ。但是我们的CPU不能并行运行这三件事,所以选择了并发来实现所谓的并行。JVM的线程调度是采用抢占式调度,谁抢到了车道谁就运行。这样造成了多线程的随机性。
线程的实现
继承Thread类
然后覆盖其中的run()方法,在其中写上你要执行的代码,然后new出来调用其start()方法即可
public class ThreadDemo extends Thread{
@Override
public void run(){
//TODO
}
}
ThreadDemo threadDemo=new ThreadDemo();
threadDemo.start();//来启动线程
实现runnable接口
runnable实际上也是调用了Thread的run()来实现的,但是因为是实现接口,这个类并不像上一个方法那样属于线程类。用runnable创建线程
Thread thread=new Thread(Runnable target);
//还有一种构造方法可以给线程起名字
Thread thread=new Thread(Runnable target,String name);
线程不安全
为了解决线程不安全的问题我们需要介绍一个新的关键字synchronized。java中的锁也与其息息相关,锁这个字眼一听就非常的牛啤。实际上我觉得也确实牛啤。
同步锁
synchronized
synchronized(){
//TODO
}
圆括号内填写的是监听对象,通常是线程共同访问的对象。synchronized会保证进行原子操作,线程挨个进入到TODO里面。
同步方法
用synchronized修饰的方法就是同步方法。换句话来说synchronized既可以像上面使用也可以作为一个修饰符使用。那么它的监听对象又是谁呢?
- 非static方法:同步锁就是this。
- static方法:同步锁是当前方法所在类的字节码对象。
lock
lock是一个接口,是一个比synchronized更为强大的锁。
Reentrant(adj.再进入的)Lock
这是API中给出的示例代码
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds.Acquires the lock.
try {
// ... method body
} finally {
lock.unlock()//Attempts to release this lock.
}
}
}
while和if的问题
在wait()方法中,官方推荐的的代码是
while(boolean){
x.wait();
}
//而不是
if(boolean){
x.wait();
}
在视频中给出的消费者生产者的案例中if也没有出错,所以当时没有想明白。不过今天看了一篇博客解决了我疑惑。
一个ThreadA在被wait了以后会被丢到等待池中等待别的Threadanother的notify才能进入锁池抢锁。接下来ThreadA抢到锁后,会执行wait接下来的语句而不是从所在方法从头开始运行。那么问题来了有时候我们的ThreadA线程抢到锁以后需要再次进行判断能不能启动,因为notify方法是随机唤醒一个Thread,有可能被唤醒的时候并不符合我们想要的时机,这种情况我们就需要再判断一次**ThreadA是不是达到了唤醒的时机。因此while更加合理,它会重新回去判断一次,如果符合那么就执行wait下的语句,如果不符合那么就进入while让ThreadA再次wait**把ta丢到等待池里去。
/*
* 生产和消费
*/
package hello;
public class SynStack {
private char[] data = new char[6];
private int cnt = 0; //表示数组有效元素的个数
public synchronized void push(char ch){
//把if改成while就没有问题了
if (cnt >= data.length){
try{
System.out.println("生产线程"+
Thread.currentThread().getName()+
"准备休眠");
this.wait();
System.out.println("生产线程"+
Thread.currentThread().getName()+
"休眠结束了");
}catch (Exception e){
e.printStackTrace();
}
}
this.notifyAll();
data[cnt] = ch;
++cnt;
System.out.printf("生产线程"+
Thread.currentThread().getName()+
"正在生产第%d个产品,该产品是: %c\n", cnt, ch);
}
public synchronized char pop(){
char ch;
//把if改成while就没有问题了
if (cnt <= 0){
try{
System.out.println("消费线程"+
Thread.currentThread().getName()+
"准备休眠");
this.wait();
System.out.println("消费线程"+
Thread.currentThread().getName()+
"休眠结束了");
}catch (Exception e){
e.printStackTrace();
}
}
this.notify();
ch = data[cnt-1];
System.out.printf("消费线程"+
Thread.currentThread().getName()+
"正在消费第%d个产品,该产品是: %c\n", cnt, ch);
--cnt;
return ch;
}
public static void main(String[] args){
SynStack ss = new SynStack();
Producer p = new Producer(ss);
Consumer c = new Consumer(ss);
Thread t1 = new Thread(p);
t1.setName("1号");
Thread t6 = new Thread(c);
t6.setName("6号");
Thread t7 = new Thread(c);
t7.setName("7号");
t1.start();
t6.start();
t7.start();
}
}
class Producer implements Runnable{
private SynStack ss = null;
public Producer(SynStack ss){
this.ss = ss;
}
public void run(){
char ch;
for (int i=0; i<10; ++i){
ch = (char)('a'+i);
ss.push(ch);
}
}
}
class Consumer implements Runnable{
private SynStack ss = null;
public Consumer(SynStack ss){
this.ss = ss;
}
public void run(){
for (int i=0; i<10; ++i){
ss.pop();
}
}
}
这里的例子其实就是参照了上面那篇博客的,非常感谢博主写的如此详尽。
我眼中的线程
需要有同步的时候
当然这幅图并不十分准确,大概的意思是这样。描述的是在一个需要同步的地方,出现的抢==锁🔒情况,以及抢锁🔒==之后的何去何从😃。
没有同步的时候
如果没有需要同步的时候,而现在我们的又是多核处理器的,这种时候就会是并行运行了。👍
后台线程
CG线程是典型的后台线程,后台线程的生命随着前台线程的消失而消失。
thread.setDaemon()//此方法将一格线程变为后台线程
setDaemon方法必须在start方法前调用,一旦进入了running状态,就不可再设置为后台线程了。
线程优先级
优先级的高低只是决定该线程获得更多执行的机会而不是一次执行完也不是必须先执行。
推荐使用Java内置的3个优先等级
MAX_PRIORITY=10
MIN_PRIORTY=1
NORMAL_PRIORTY=5
hread.setDaemon()//此方法将一格线程变为后台线程
setDaemon方法必须在start方法前调用,一旦进入了running状态,就不可再设置为后台线程了。
一个线程的子线程会继承它的优先级,不过子线程的优先级可以进行修改。