tosting 方法
public class ClassA { private String name; private int age=1; @Override//方法的重写 public String toString() { //为了直接获取A类的名和变量值 return "ClassA [name=" + name + ", age=" + age + "]"; } public static void main(String[] args) { ClassA a1 = new ClassA(); System.out.println(a1); } }
该种方法,可以用来检查代码的内容的正确性。
当一个共享变量被volatile修饰时,它会保证修改的值立即被更新到主存“, 这里的”保证“ 是如何做到的?和 JIT的具体编译后的CPU指令相关吧?
volatile特性
内存可见性:通俗来说就是,线程A对一个volatile变量的修改,对于其它线程来说是可见的,即线程每次获取volatile变量的值都是最新的。
volatile的使用场景
通过关键字sychronize可以防止多个线程进入同一段代码,在某些特定场景中,volatile相当于一个轻量级的sychronize,因为不会引起线程的上下文切换,但是使用volatile必须满足两个条件:
1、对变量的写操作不依赖当前值,如多线程下执行a++,是无法通过volatile保证结果准确性的;
2、该变量没有包含在具有其它变量的不变式中,这句话有点拗口(目前本人暂未研究明白)。
public class TestVolatile { public static void main(String[] args) { ThreadDemo td = new ThreadDemo(); new Thread(td).start();//启动一个线程,start方法会去调用下面重写的run方法 while (true) { if (td.isFlag()){ System.out.println("xxxxxxxxxxxxxxx"); break; } } } } //从上面知道了,该函数一共会启动两个线程,并且这个两个线程共用了flag变量,(同进程中,线程是共享进程中的数据) class ThreadDemo implements Runnable { private boolean flag = false; @Override public void run() { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("flag= " + flag); } public boolean isFlag() { return flag; } }
从执行结果可以看到:在有些情况下,不能执行以下语句:System.out.println("xxxxxxxxxxxxxxx");
原因:就是因为未进行内存可见性,因为主线程和次线程从工作内存中拷贝的flag都是false。
public class TestVolatile { public static void main(String[] args) { ThreadDemo td = new ThreadDemo(); new Thread(td).start(); while (true) { if (td.isFlag()){ System.out.println("xxxxxxxxxxxxxxx"); break; } } } } class ThreadDemo implements Runnable { private volatile boolean flag = false;//仅仅在此处多加了一个关键字:volatile @Override public void run() { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; System.out.println("flag= " + flag); } public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } }
从执行结果可以看到:在有些情况下,执行了以下语句:System.out.println("xxxxxxxxxxxxxxx");
原因:就是因为进行了内存可见性。
如何保证内存可见性?
下面看看操作普通变量和volatile变量有什么不同:
1、对于普通变量:读操作会优先读取工作内存的数据,如果工作内存中不存在,则从主内存中拷贝一份数据到工作内存中;写操作只会修改工作内存的副本数据,这种情况下,其它线程就无法读取变量的最新值。
2、对于volatile变量,读操作时JMM会把工作内存中对应的值设为无效,要求线程从主内存中读取数据;写操作时JMM会把工作内存中对应的数据刷新到主内存中,这种情况下,其它线程就可以读取变量的最新值。
CPU为了处理性能,并不直接和内存进行通信,而是将内存的数据读取到内部缓存(L1,L2)再进行操作,但操作完并不能确定何时写回到内存,如果对volatile变量进行写操作,当CPU执行到Lock前缀指令时,会将这个变量所在缓存行的数据写回到内存,不过还是存在一个问题,就算内存的数据是最新的,其它CPU缓存的还是旧值,所以为了保证各个CPU的缓存一致性,每个CPU通过嗅探在总线上传播的数据来检查自己缓存的数据有效性,当发现自己缓存行对应的内存地址的数据被修改,就会将该缓存行设置成无效状态,当CPU读取该变量时,发现所在的缓存行被设置为无效,就会重新从内存中读取数据到缓存中。
但是volatile是原子性的吗?
public class Test { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
大家想一下这段程序的输出结果是多少?也许有些朋友认为是10000。但是事实上运行结果每次不一致而且都是一个小于10000的数字。
可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操作,由于volatile保证了可见性,那么在每个线程中对inc自增完之后,在其他线程中都能看到修改后的值啊,所以有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000。
假如某时刻变量inc的值为10,
线程1对变量进行自增操作,线程1先读取了变量inc值10,然后线程1被阻塞了(注意时间点:线程1已 经读取了内存数据,并往下开始执行了,在此时他被阻塞了。);
然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,还没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。
然后线程1接着进行加1操作,由于已经读取了inc的值,注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。
那么两个线程分别进行了一次自增操作后,inc却只增加了1。
解释到这里,可能有朋友会有疑问,不对啊,前面不是保证一个变量在修改volatile变量时,会让缓存行无效吗?我们一定要注意时间点,线程1对变量进行读取操作之后,被阻塞了的话,并没有对inc值进行修改。然后虽然volatile能保证线程2对变量inc的值读取是从内存中读取的,但是线程1没有进行修改,所以线程2根本就不会看到修改的值。
这里面的误区是:volatile关键字能保证可见性没有错,但是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,但是volatile没办法保证对变量的操作的原子性。注意:自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的 一共包含三个子操作,由于时间片的原因,这三步就由可能会分割开执行。那就导致线程1读取了inc ,然后就被切换了时间片,此时线程2又去读inc值。并往下执行。
解释原子性:https://www.cnblogs.com/dolphin0520/p/3920373.html
父类构造方法和子类构造方法
1.当有构造方法的时候,建议再写一份无参数的构造方法,
2.当父类有构造方法,子类必须写构造方法,并在构造方法中去调用父类的构造方法
public class tostring { public String name="xxx"; int age; public tostring(int age){ //有参数的构造方法 this.age=age; } public tostring(){//无参数的构造方法,如果需调用,就必须写 this.age=age; } public static void main(String[] args) { tostring a1 = new tostring();//调用无参构造方法 tostring a2 = new tostring(1);//调用有参构造方法 } }
public class ClassA extends tostring{ public ClassA(int age) { //只能使用子类的构造放方法去调用父类构造方法 super(age); //调用父类构造方法 // TODO Auto-generated constructor stub } public static void main(String[] args) { tostring a1 = new tostring(); //直接调用无参的构造方法 ClassA aa=new ClassA(10); System.out.println(aa.age); //调用父类的变量 System.out.println(a1.name); } }
从以上学习,我们应该对子父类的继承有所认识了。
浙公网安备 33010602011771号