int index=0;
char data[]=new char[6];
public synchronized void push(char c){
data[index]=c;
System.out.println("Push char: "+c+" ->");
try{
Thread.sleep(1000);
}
catch(Exception e){}
index++;
System.out.println("--> Push "+c+" Completed index="+index+" Stack is "+data[0]+" "+data[1]+" "+data[2]);
}
public void pop(){
index--;
try{
Thread.sleep(10);
}
catch(Exception e){}
char c=data[index];
data[index]=' ';
System.out.println("** Pop "+c+"** index="+index+" Stack is "+data[0]+" "+data[1]+" "+data[2]);
}
}
class PushRunner implements Runnable {
private Stack s;
public PushRunner(Stack s){
this.s=s;
}
public void run(){
s.push('c');
}
}
class PopRunner implements Runnable {
private Stack s;
public PopRunner(Stack s){
this.s=s;
}
public void run(){
synchronized(s){
s.pop();
}
}
}
public class TestSynchronize {
public static void main(String[] args){
Stack s=new Stack();
s.push('a');
s.push('b');
new Thread(new PushRunner(s)).start();
new Thread(new PopRunner(s)).start();
}
}
首先,我们讨论一下进程,进程是操作系统级别下,单独执行的一个任务。
Win32 Unix都是多任务操作系统。
多任务,并发执行是一个宏观概念,实际微观串行。
CPU同一时间刻只能执行一个任务。
OS负责进程调度,使用CPU,也就是获得时间片。
在一个进程中,再可以分为多个程序顺序执行流,每个执行流就是一个线程。
分配CPU时间片的依然是CPU,多线程时,程序会变慢,每个线程分配到的时间片少了。
进程与线程的区别,进程是数据独占的(独立数据空间),线程是数据共享的(这也是线程之间通讯容易的原因,不需要传递数据)。
Java是语言级支持多线程的,体现在有现成的封装类(java.lang.Thread)完成了必要的并发细节的工作(与操作系统打交道,分配PID等)。
两种方式来得到一个线程对象。
public static void main(String[] args)
{
Thread t1 = new MyThread();
t1.start();
}
}
class MyThread extends Thread
{
public void run()
{
System.out.println("thread run");
}
}
一个线程对象--〉代表着一个线程--〉一个顺序执行流(run方法)
这个程序有两个线程,一个是main主线程,它调用了t1.start(),这是t1线程只是就绪状态,还没有真正启动线程,main主线程结束了!!t1运行。两个线程都退出了,进程完结。
一个进程退出,要等待进程中所有线程都退出,再退出虚拟机。
方式2,实现java.lang.Runable接口,这是这个类的对象是一个目标对象,而不能理解为是一个线程对象。
public static void main(String[] args)
{
Thread t1 = new MyThread();
t1.start();
Runnable target = new MyRunnable();
Thread t2 = new Thread(target);
t2.start();
}
}
class MyThread extends Thread
{
public void run()
{
System.out.println("thread run");
}
}
class MyRunnable implements Runnable
{
public void run()
{
System.out.println("runnable run");
}
}
不要调用run()方法,它只是执行一下普通的方法,并不会启动单独的线程。
上面只是线程状态图。
在某一个时间内,处于运行状态的线程,执行代码,注意可能多个线程多次执行代码。
CPU会不断从可运行状态线程调入运行,不会让CPU空闲。
Thread.sleep(1000);当前线程睡眠1秒钟,休眠后->进入阻塞->休眠结束->回到可运行状态。
在run(),有异常抛出,必须try{}catch(Exception e){},不能throws Exception,因为run()方法覆盖不能抛例外。
能进入运行状态,只能由操作系统来调度。
一旦sleep-〉阻塞->交出程序执行权。
等待用户输入,输入输出设备占用CPU,处于阻塞的线程没有机会运行,输入完毕,重新进入可运行状态。
第三种进入阻塞状态的可能。
t1.join()调用后,运行状态线程放出执行权,作为t1的后续线程,等待t1结束。也就是说至少得等t1线程run完毕,才可能进入运行状态来执行,可不是说t1执行完,一定马上就是调用t1.join()的线程马上进入可运行行状态。只有操作系统有权利决定谁进入运行状态。
join的实质就是让两个线程和二为一,串行。
t1.join();执行这条语句现场是被保护起来的。t1结束,调用线程有机会运行时,会从上次的位置继续运行。
public static void main(String[] args)
{
Thread t1 = new MyThread();
t1.start();
for(int i = 0; i < 50; i++)
{
System.out.println("#");
if(i == 24)
try{
t1.join();
}catch(InterruptedException e)
{
e.printStackTrace();
}
}
}
}
class MyThread extends Thread
{
public void run()
{
for(int i = 0; i < 35; i++)
{
System.out.println("*");
}
}
}
线程优先级,setPriotity(1--100),数越大,优先级越高。
开发中不提倡自省设置优先级,操作系统可能忽略优先级,不具有跨平台性(两方面,可运行,执行效果一致),因为这种方式很粗略。
static void yield(),运行状态的线程(当前线程),调用yield方法,马上交出执行权。回到可运行状态。
=============================
Thread对象有个run方法,当start()时,Thread进行系统级调用,系统分配一个线程空间,此时对象可以获得CPU时间片,一个顺序执行流程可以独立运行,线程结束,对象还在,只是系统回收线程。
=============================
两个线程同时的资源,称为临界资源,会有冲突。
堆栈数据结构,有一个char[]和一个index(表示实际长度,也表示下一个要插入元素的位置)。
一个push操作,有两个核心操作(加元素,修改index)。都执行和都没执行,没有问题。
但假设一个线程做了一个步,就交出执行权,别的线程,执行同样的代码,会造成数据不一致。
数据完整性也是一个要在开发中注意的地方。
------------------------------------------
所以为了保证数据安全,要给数据加锁。
一个Java对象,不仅有属性和方法,还有别的东西。
任何一个对象,都有一个monitor,互斥锁标记,可以交给一个线程。
只有拿到这个对象互斥锁标记的线程,才能访问这个对象。
synchronized可以修饰方法和代码块。
synchronized(obj){
obj.setValue(123);
}
不是每个线程都能进入这段代码块,只有拿到锁标记的线程才能进入执行完,释放锁标记,给下一个线程。
记住,锁标记是对对象来说的,锁的是对象。
当synchronized标识方法时,那么就是锁当前对象。
int index=0;
char data[]=new char[6];
public synchronized void push(char c){
data[index]=c;
System.out.println("Push char: "+c+" ->");
try{
Thread.sleep(1000);
}
catch(Exception e){}
index++;
System.out.println("--> Push "+c+" Completed index="+index+" Stack is "+data[0]+" "+data[1]+" "+data[2]);
}
public void pop(){
index--;
try{
Thread.sleep(10);
}
catch(Exception e){}
char c=data[index];
data[index]=' ';
System.out.println("** Pop "+c+"** index="+index+" Stack is "+data[0]+" "+data[1]+" "+data[2]);
}
}
class PushRunner implements Runnable {
private Stack s;
public PushRunner(Stack s){
this.s=s;
}
public void run(){
s.push('c');
}
}
class PopRunner implements Runnable {
private Stack s;
public PopRunner(Stack s){
this.s=s;
}
public void run(){
synchronized(s){
s.pop();
}
}
}
public class TestSynchronize {
public static void main(String[] args){
Stack s=new Stack();
s.push('a');
s.push('b');
new Thread(new PushRunner(s)).start();
new Thread(new PopRunner(s)).start();
}
}
注意此代码中,Stack这个临界资源类中的push方法中,有一个Thread.sleep,它让当前进程阻塞,也就是让拥有Stack对象s锁标记的线程阻塞,但这时它并不释放锁标记。
所以Synchronized使用是有代价的,牺牲效率换数据安全,要控制synchronized代码块,主要是数据写,修改做同步限制,读就不用了。
还有一点要注意:synchronized不能继承, 父类的方法是synchronized,那么其子类重载方法中就不会继承“同步”。
一个线程可以拥有很多对象锁标记,但一个对象的锁标记只能给一个线程。
等待锁标记的线程,进入该对象的锁池。
每个对象都有一个空间,锁池,里面都是等待拿到该对象的锁标记的线程。
当然还是操作系统来决定谁来获得锁标记,在上一个锁标记释放掉后。
死锁,线程A拿到resourceA标记,去请求resourceB;线程B拿到resourceB标记,去请求resourceA;
线程间通讯机制->协调机制
一个对象不仅有锁和锁池,另外还有一个空间[等待队列]。
synchronized(路南){
想要获得路北资源的线程,调用路南.wait();将自己的所有锁标记都释放。以便其他线程满足条件运行程序后,自己也就可以正常通过了。
}
调用obj.wait(),表示某一个线程释放所有锁标记并进入obj这个对象的等待队列。
等待队列也是阻塞状态。一个线程调用obj对象的notify(),会通知等待队列中的一个线程可以出来,notifyAll()是通知所有线程。
public static void main(String[] args){
SyncStack s=new SyncStack();
Runnable p=new Producer(s);
Runnable c=new Consumer(s);
new Thread(p).start();
new Thread(c).start();
}
}
class Producer implements Runnable {
SyncStack s;
public Producer(SyncStack s){
this.s=s;
}
public void run() {
for(int i=1;i<=20;i++){
char c=(char)(Math.random()*26+'A');
s.push(c);
try{
Thread.sleep(20);
}
catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
class Consumer implements Runnable {
SyncStack s;
public Consumer(SyncStack s){
this.s=s;
}
public void run(){
for(int i=1;i<=20;i++){
char c=s.pop();
try{
Thread.sleep(400);
}
catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
class SyncStack {
private int index=0; // the index next char added into, also presents the number of chars in stack
private char[] data=new char[6];
public synchronized void push(char c) {
while (index==data.length) {
try{
this.wait();
}
catch (InterruptedException e){}
}
data[index]=c;
index++;
System.out.println("Char "+c+" Pushed into Stack");
for(int k=0;k<data.length;k++) System.out.print(data[k]);
System.out.println();
this.notifyAll();
}
public synchronized char pop() {
while (index==0) {
try{
this.wait();
}
catch (InterruptedException e){}
}
index--;
char c=data[index];
data[index]=' ';
System.out.println("Char "+c+" Poped from Stack");
for(int k=0;k<data.length;k++) System.out.print(data[k]);
System.out.println();
this.notifyAll();
return c;
}
}
上面为经典的生产者消费者问题,生产者使用SyncStack的push方法,消费者使用pop方法。
push方法:
while (index==data.length) {
try{
this.wait();//<-----------释放所有锁标记,阻塞现场保留
}
catch (InterruptedException e){}
}
如果货架满了,生产者即使拥有锁标记,也不能再生产商品了,必须wait()。等待消费者消费物品,否则永远不会从SyncStack对象的等待队列中出来。
等待通知,何时通知呢?
public synchronized char pop() {
while (index==0) {
try{
this.wait();
}
catch (InterruptedException e){}
}
index--;
char c=data[index];
data[index]=' ';
System.out.println("Char "+c+" Poped from Stack");
for(int k=0;k<data.length;k++) System.out.print(data[k]);
System.out.println();
this.notifyAll(); //<-------------所有等待队列中的生产者都出了队列,因为没有锁标记,只能进入锁池。
return c;
}
为什么判断是一个while循环,而不是一个if呢?
注意:我们假设一种情形:
1) 有十个生产者线程,货架已经满了,10生产者依次获得锁标记,依次都调用this.wait(),都进入同一个SyncStack对象的等待队列,10个进程阻塞住,
2) 有一个消费者线程获得该SyncStack对象锁标记,一个消费者消费一个,执行完消费,调用this.notifyAll()
释放锁标记。
3) 刚才消费者调用SyncStack对象的notifyAll()后,10个线程都出来了,准备生产商品,全部进入锁池。
这十个线程的代码现场,还在wait()这个函数调用后面,也就是一旦或者锁标记,要继续从这里执行。
this.wait();//从这一句的后面继续执行。
4) 但如果有一个生产者push的话,货架已经就满了,但这时还有9个在锁池中,依次获得锁标记,但由于是while需要再次判断是否货架满不满,才能继续前行进行生产。如果是if,就会直接push,数组越界。
===================================
释放锁标记只有两种途径,代码执行完,wait()
让线程结束,就是想办法让run方法结束。
注意下面的bStop,标志位,可以在线程进入wait状态时,对某一线程调用interrupt(),线程抛出InterruptedException,然后根据标志位,方法返回。
{
public static void main(String[] args)
{
Thread1 t1=new Thread1();
t1.start();
int index=0;
while(true)
{
if(index++==500)
{
t1.stopThread();
t1.interrupt();
break;
}
System.out.println(Thread.currentThread().getName());
}
System.out.println("main() exit");
}
}
class Thread1 extends Thread
{
private boolean bStop=false;
public synchronized void run()
{
while(!bStop)
{
try
{
wait();
}
catch(InterruptedException e)
{
//e.printStackTrace();
if(bStop) return;
}
System.out.println(getName());
}
}
public void stopThread()
{
bStop=true;
}
}
Exc:
AB1CD2....
数字与字母依次打印。用线程完成。
public static void main(String[] args)
{
Object o = new Object();
PrintChar pc = new PrintChar(o);
PrintNum pn = new PrintNum(o);
pn.start();
pc.start();
}
}
class PrintChar extends Thread {
private int index = 0;
private Object obj = null;
public PrintChar(Object o)
{
this.obj = o;
}
public void run(){
synchronized(obj){
for( ;index < 26; index++)
{
System.out.print((char)(index+'A'));
obj.notifyAll();
if(index != 25)
try{
obj.wait();
}catch(InterruptedException e){}
}
}
}
}
class PrintNum extends Thread {
private int index = 1;
private Object obj = null;
public PrintNum(Object o)
{
this.obj = o;
}
public void run(){
synchronized(obj){
for( ;index < 53;index++)
{
System.out.print(index);
if(index%2==0){
obj.notifyAll();
if(index != 52){
try{
obj.wait();
}catch(InterruptedException e)
{}
}
}
}
}
}
}
|
|